@swarmclawai/swarmclaw 1.9.27 → 1.9.29

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
@@ -151,14 +151,14 @@ clawhub install swarmclaw
151
151
 
152
152
  [Browse on ClawHub](https://clawhub.ai/skills/swarmclaw)
153
153
 
154
- ## v1.9.27 Highlights
154
+ ## v1.9.29 Highlights
155
155
 
156
- Desktop compatibility and provider-save repair for Intel Mac users and OpenRouter setup.
156
+ Issue-fix release for Edit Agent tooltips, installed package builds, and structured dream output on local Ollama models.
157
157
 
158
- - **Intel macOS native modules.** The desktop packaging hook now rebuilds Electron-loaded native modules with the target architecture and blocks a release if an x64 macOS bundle contains an arm64-only required addon.
159
- - **OpenRouter save repair.** Provider updates now tolerate UI metadata fields like `id`, `type`, `createdAt`, and `updatedAt` without persisting them, while still rejecting unrelated unknown fields.
160
- - **Downloads clarity.** The downloads page no longer guesses Apple Silicon when a browser hides the Mac architecture, so Intel users can choose the x64 DMG explicitly.
161
- - **Regression coverage.** Provider route and Electron after-pack tests cover the reported failure modes.
158
+ - **Edit Agent tooltips.** Help tips in the Edit Agent sheet now render above modal layers instead of being hidden behind the dialog.
159
+ - **Installed package builds.** The npm package now ships the Dagre type declarations needed by `swarmclaw server --build`.
160
+ - **Local Ollama dream output.** Structured dream/reflection calls request Ollama JSON mode and validate balanced JSON before writing memories.
161
+ - **Regression coverage.** CLI/package, model-build, and dream-parser tests cover the reported failure modes.
162
162
 
163
163
  ## Hosted Deploys
164
164
 
@@ -410,6 +410,25 @@ Operational docs: https://swarmclaw.ai/docs/observability
410
410
 
411
411
  ## Releases
412
412
 
413
+ ### v1.9.29 Highlights
414
+
415
+ Issue-fix release for Edit Agent tooltips, installed package builds, and structured dream output on local Ollama models.
416
+
417
+ - **Edit Agent tooltips.** Help tips in the Edit Agent sheet now render above modal layers instead of being hidden behind the dialog.
418
+ - **Installed package builds.** The npm package now ships the Dagre type declarations needed by `swarmclaw server --build`.
419
+ - **Local Ollama dream output.** Structured dream/reflection calls request Ollama JSON mode and validate balanced JSON before writing memories.
420
+ - **Regression coverage.** CLI/package, model-build, and dream-parser tests cover the reported failure modes.
421
+
422
+ ### v1.9.28 Highlights
423
+
424
+ Issue-fix release for installed CLI groups, email bridge TLS handling, built-in model overrides, and Windows desktop native modules.
425
+
426
+ - **Installed CLI groups.** Global npm installs route legacy API-backed group commands through the bundled TS runtime when installed under `node_modules`, avoiding Node 22.6+/25 type-stripping failures.
427
+ - **Email bridge TLS resilience.** The email connector logs IMAP socket errors without crashing the daemon and supports `tlsRejectUnauthorized=false` for local self-signed IMAP/SMTP servers.
428
+ - **Provider model override persistence.** Built-in provider live model saves now reload array-valued overrides instead of falling back to catalog defaults.
429
+ - **Windows desktop native modules.** Desktop packaging syncs rebuilt Electron-native modules into traced `.next/node_modules` aliases so packaged Windows installs start against the correct ABI.
430
+ - **Regression coverage.** CLI, email, provider route, and Electron after-pack tests cover the reported failure modes.
431
+
413
432
  ### v1.9.27 Highlights
414
433
 
415
434
  Desktop compatibility and provider-save repair for Intel Mac users and OpenRouter setup.
package/bin/swarmclaw.js CHANGED
@@ -55,9 +55,19 @@ function hasTsxRuntime() {
55
55
  }
56
56
  }
57
57
 
58
+ function pathIsInsideNodeModules(filePath) {
59
+ return path.resolve(filePath).split(path.sep).includes('node_modules')
60
+ }
61
+
58
62
  function buildLegacyTsCliArgs(cliPath, argv, options = {}) {
63
+ const ext = path.extname(cliPath).toLowerCase()
64
+ if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
65
+ return [cliPath, ...argv]
66
+ }
67
+
68
+ const insideNodeModules = options.insideNodeModules ?? pathIsInsideNodeModules(cliPath)
59
69
  const stripTypesSupported = options.supportsStripTypes ?? supportsStripTypes()
60
- if (stripTypesSupported) {
70
+ if (stripTypesSupported && !insideNodeModules) {
61
71
  return ['--no-warnings', '--experimental-strip-types', cliPath, ...argv]
62
72
  }
63
73
 
@@ -69,6 +79,10 @@ function buildLegacyTsCliArgs(cliPath, argv, options = {}) {
69
79
  return null
70
80
  }
71
81
 
82
+ function resolveLegacyTsCliPath() {
83
+ return path.join(__dirname, '..', 'src', 'cli', 'index.ts')
84
+ }
85
+
72
86
  function normalizeLegacyTsCliArgv(argv) {
73
87
  const normalized = []
74
88
 
@@ -98,7 +112,7 @@ function normalizeLegacyTsCliArgv(argv) {
98
112
  }
99
113
 
100
114
  function runLegacyTsCli(argv) {
101
- const cliPath = path.join(__dirname, '..', 'src', 'cli', 'index.ts')
115
+ const cliPath = resolveLegacyTsCliPath()
102
116
  const args = buildLegacyTsCliArgs(cliPath, normalizeLegacyTsCliArgv(argv))
103
117
  const env = normalizeLegacyCliEnv(process.env)
104
118
  if (!args) {
@@ -358,6 +372,8 @@ module.exports = {
358
372
  buildLegacyTsCliArgs,
359
373
  hasTsxRuntime,
360
374
  normalizeLegacyTsCliArgv,
375
+ pathIsInsideNodeModules,
376
+ resolveLegacyTsCliPath,
361
377
  TS_CLI_ACTIONS,
362
378
  normalizeLegacyCliEnv,
363
379
  printPackageVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.9.27",
3
+ "version": "1.9.29",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
@@ -88,7 +88,7 @@
88
88
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/electron-signing-config.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
89
89
  "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 src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
90
90
  "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/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.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/session-tools/swarmdock.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/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
91
- "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/connectors/slack.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-timing.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/gateways/control-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
91
+ "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/memory/dream-service.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/connectors/slack.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-timing.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/gateways/control-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
92
92
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
93
93
  "test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
94
94
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -119,6 +119,7 @@
119
119
  "@tailwindcss/postcss": "^4",
120
120
  "@tanstack/react-query": "^5.91.0",
121
121
  "@types/better-sqlite3": "^7.6.13",
122
+ "@types/dagre": "^0.7.54",
122
123
  "@types/mailparser": "^3.4.6",
123
124
  "@types/node": "^20",
124
125
  "@types/nodemailer": "^7.0.11",
@@ -179,7 +180,6 @@
179
180
  },
180
181
  "devDependencies": {
181
182
  "@electron/rebuild": "^3.7.2",
182
- "@types/dagre": "^0.7.54",
183
183
  "electron": "^33.3.0",
184
184
  "electron-builder": "^25.1.8",
185
185
  "eslint": "^9",
@@ -58,3 +58,39 @@ test('provider models route updates custom provider configs without creating mod
58
58
  hasOverride: false,
59
59
  })
60
60
  })
61
+
62
+ test('provider model overrides preserve built-in provider array rows', () => {
63
+ const output = runWithTempDataDir<{
64
+ overrides: Record<string, string[]>
65
+ providerModels: string[]
66
+ getPayload: { models: string[]; hasOverride: boolean }
67
+ }>(`
68
+ const storageMod = await import('./src/lib/server/storage')
69
+ const providerMod = await import('./src/lib/providers')
70
+ const routeMod = await import('./src/app/api/providers/[id]/models/route')
71
+ const storage = storageMod.default || storageMod
72
+ const providers = providerMod.default || providerMod
73
+ const route = routeMod.default || routeMod
74
+
75
+ storage.saveModelOverrides({ lmstudio: ['qwen3.5-27b'] })
76
+
77
+ const getResponse = await route.GET(
78
+ new Request('http://local/api/providers/lmstudio/models'),
79
+ { params: Promise.resolve({ id: 'lmstudio' }) },
80
+ )
81
+ const provider = providers.getProviderList().find((entry) => entry.id === 'lmstudio')
82
+
83
+ console.log(JSON.stringify({
84
+ overrides: storage.loadModelOverrides(),
85
+ providerModels: provider?.models || [],
86
+ getPayload: await getResponse.json(),
87
+ }))
88
+ `, { prefix: 'swarmclaw-provider-model-override-test-' })
89
+
90
+ assert.deepEqual(output.overrides, { lmstudio: ['qwen3.5-27b'] })
91
+ assert.deepEqual(output.providerModels, ['qwen3.5-27b'])
92
+ assert.deepEqual(output.getPayload, {
93
+ models: ['qwen3.5-27b'],
94
+ hasOverride: true,
95
+ })
96
+ })
@@ -7,7 +7,7 @@ const fs = require('node:fs')
7
7
  const os = require('node:os')
8
8
  const path = require('node:path')
9
9
  const { spawnSync } = require('node:child_process')
10
- const { buildLegacyTsCliArgs } = require('../../bin/swarmclaw.js')
10
+ const { buildLegacyTsCliArgs, resolveLegacyTsCliPath } = require('../../bin/swarmclaw.js')
11
11
 
12
12
  const CLI_BIN = path.join(__dirname, '..', '..', 'bin', 'swarmclaw.js')
13
13
  const PACKAGE_JSON = require('../../package.json')
@@ -199,6 +199,12 @@ test('binary -v alias output matches package version', () => {
199
199
  assert.equal(result.stdout.trim(), `${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`)
200
200
  })
201
201
 
202
+ test('package ships dagre type declarations required by installed builds', () => {
203
+ assert.equal(PACKAGE_JSON.dependencies.dagre, '^0.8.5')
204
+ assert.equal(PACKAGE_JSON.dependencies['@types/dagre'], '^0.7.54')
205
+ assert.equal(PACKAGE_JSON.devDependencies?.['@types/dagre'], undefined)
206
+ })
207
+
202
208
  test('legacy TS launcher falls back to tsx import when strip-types is unavailable', () => {
203
209
  const cliPath = path.join(APP_ROOT, 'src', 'cli', 'index.ts')
204
210
  const args = buildLegacyTsCliArgs(cliPath, ['runs', 'list'], {
@@ -208,3 +214,14 @@ test('legacy TS launcher falls back to tsx import when strip-types is unavailabl
208
214
 
209
215
  assert.deepEqual(args, ['--no-warnings', '--import', 'tsx', cliPath, 'runs', 'list'])
210
216
  })
217
+
218
+ test('legacy TS launcher uses tsx instead of strip-types inside node_modules', () => {
219
+ const cliPath = path.join(os.tmpdir(), 'node_modules', '@swarmclawai', 'swarmclaw', 'src', 'cli', 'index.ts')
220
+ const args = buildLegacyTsCliArgs(cliPath, ['agents', 'list'], {
221
+ supportsStripTypes: true,
222
+ hasTsxRuntime: true,
223
+ })
224
+
225
+ assert.deepEqual(args, ['--no-warnings', '--import', 'tsx', cliPath, 'agents', 'list'])
226
+ assert.equal(resolveLegacyTsCliPath(), path.join(APP_ROOT, 'src', 'cli', 'index.ts'))
227
+ })
@@ -42,7 +42,7 @@ function TooltipContent({
42
42
  data-slot="tooltip-content"
43
43
  sideOffset={sideOffset}
44
44
  className={cn(
45
- "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
45
+ "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[1300] w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
46
46
  className
47
47
  )}
48
48
  {...props}
@@ -250,6 +250,47 @@ test('buildChatModel disables parallel_tool_calls for Ollama local to avoid dupl
250
250
  assert.equal(llm.clientConfig?.baseURL, 'http://localhost:11434/v1')
251
251
  })
252
252
 
253
+ test('buildChatModel passes JSON format mode only to local Ollama structured calls', () => {
254
+ const local = buildChatModel({
255
+ provider: 'ollama',
256
+ model: 'gemma4:e4b',
257
+ ollamaMode: 'local',
258
+ apiKey: null,
259
+ responseFormat: 'json_object',
260
+ }) as ChatOpenAI & { modelKwargs?: Record<string, unknown> }
261
+
262
+ assert.equal(local.modelKwargs?.parallel_tool_calls, false)
263
+ assert.equal(local.modelKwargs?.format, 'json')
264
+
265
+ saveCredentials({
266
+ 'cred-ollama-cloud-json': {
267
+ id: 'cred-ollama-cloud-json',
268
+ provider: 'ollama',
269
+ name: 'Ollama Cloud',
270
+ encryptedKey: encryptKey('ollama-cloud-key'),
271
+ createdAt: Date.now(),
272
+ },
273
+ } as Record<string, {
274
+ id: string
275
+ provider: string
276
+ name: string
277
+ encryptedKey: string
278
+ createdAt: number
279
+ }>)
280
+
281
+ const cloud = buildChatModel({
282
+ provider: 'ollama',
283
+ model: 'glm-5:cloud',
284
+ ollamaMode: 'cloud',
285
+ apiKey: null,
286
+ credentialId: 'cred-ollama-cloud-json',
287
+ responseFormat: 'json_object',
288
+ }) as ChatOpenAI & { modelKwargs?: Record<string, unknown> }
289
+
290
+ assert.equal(cloud.modelKwargs?.parallel_tool_calls, false)
291
+ assert.equal(cloud.modelKwargs?.format, undefined)
292
+ })
293
+
253
294
  test('buildChatModel uses a reasoning_content-preserving bridge for DeepSeek', () => {
254
295
  const llm = buildChatModel({
255
296
  provider: 'deepseek',
@@ -18,6 +18,7 @@ const OLLAMA_CLOUD_URL = 'https://ollama.com/v1'
18
18
  const OLLAMA_LOCAL_URL = 'http://localhost:11434/v1'
19
19
  export const OPENAI_COMPAT_MODEL_TIMEOUT_MS = 180_000
20
20
  export const OPENAI_COMPAT_MODEL_MAX_RETRIES = 2
21
+ export type GenerationResponseFormat = 'json_object'
21
22
 
22
23
  export interface GenerationModelPreference {
23
24
  provider?: string | null
@@ -27,6 +28,7 @@ export interface GenerationModelPreference {
27
28
  apiEndpoint?: string | null
28
29
  gatewayProfileId?: string | null
29
30
  thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high' | null
31
+ responseFormat?: GenerationResponseFormat | null
30
32
  }
31
33
 
32
34
  interface ResolvedGenerationModelConfig {
@@ -36,6 +38,7 @@ interface ResolvedGenerationModelConfig {
36
38
  apiKey: string | null
37
39
  apiEndpoint: string | null
38
40
  thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high'
41
+ responseFormat?: GenerationResponseFormat
39
42
  }
40
43
 
41
44
  type OpenAiReasoningEffort = 'low' | 'medium' | 'high'
@@ -43,6 +46,7 @@ type ChatOpenAiConfig = ConstructorParameters<typeof ChatOpenAI>[0] & {
43
46
  modelKwargs?: {
44
47
  reasoning_effort?: OpenAiReasoningEffort
45
48
  parallel_tool_calls?: boolean
49
+ format?: 'json'
46
50
  }
47
51
  configuration?: {
48
52
  baseURL?: string
@@ -68,8 +72,9 @@ export function buildChatModel(opts: {
68
72
  credentialId?: string | null
69
73
  apiEndpoint?: string | null
70
74
  thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high'
75
+ responseFormat?: GenerationResponseFormat
71
76
  }) {
72
- const { provider, model, ollamaMode, apiKey, credentialId, apiEndpoint, thinkingLevel } = opts
77
+ const { provider, model, ollamaMode, apiKey, credentialId, apiEndpoint, thinkingLevel, responseFormat } = opts
73
78
  const resolvedCredentialId = resolveProviderCredentialId({ provider, ollamaMode: ollamaMode ?? null, credentialId })
74
79
  const resolvedApiKey = apiKey ?? resolveApiKeyFromCredential(resolvedCredentialId)
75
80
  const providers = getProviderList()
@@ -118,7 +123,10 @@ export function buildChatModel(opts: {
118
123
  timeout: OPENAI_COMPAT_MODEL_TIMEOUT_MS,
119
124
  maxRetries: OPENAI_COMPAT_MODEL_MAX_RETRIES,
120
125
  configuration: { baseURL },
121
- modelKwargs: { parallel_tool_calls: false },
126
+ modelKwargs: {
127
+ parallel_tool_calls: false,
128
+ ...(responseFormat === 'json_object' && !runtime.useCloud ? { format: 'json' as const } : {}),
129
+ },
122
130
  })
123
131
  }
124
132
 
@@ -222,6 +230,7 @@ function resolvePreferredGenerationConfig(
222
230
  apiKey,
223
231
  apiEndpoint,
224
232
  thinkingLevel: candidate.thinkingLevel || undefined,
233
+ responseFormat: candidate.responseFormat || undefined,
225
234
  }
226
235
  }
227
236
  return null
@@ -232,6 +241,7 @@ export function resolveGenerationModelConfig(options?: {
232
241
  sessionId?: string | null
233
242
  agentId?: string | null
234
243
  excludeProviders?: string[]
244
+ responseFormat?: GenerationResponseFormat
235
245
  }): ResolvedGenerationModelConfig {
236
246
  const providers = getProviderList()
237
247
  const excludeProviders = new Set((options?.excludeProviders || []).map((value) => normalizePreferenceValue(value)).filter(Boolean))
@@ -252,7 +262,9 @@ export function resolveGenerationModelConfig(options?: {
252
262
  ...getAgentGenerationPreferences(sessionAgent),
253
263
  ...getAgentGenerationPreferences(directAgent),
254
264
  ], excludeProviders)
255
- if (resolved) return resolved
265
+ if (resolved) return options?.responseFormat
266
+ ? { ...resolved, responseFormat: options.responseFormat }
267
+ : resolved
256
268
 
257
269
  const sessionLabel = options?.sessionId ? `session "${options.sessionId}"` : null
258
270
  const agentLabel = options?.agentId ? `agent "${options.agentId}"` : null
@@ -271,6 +283,7 @@ export async function buildLLM(options?: {
271
283
  sessionId?: string | null
272
284
  agentId?: string | null
273
285
  excludeProviders?: string[]
286
+ responseFormat?: GenerationResponseFormat
274
287
  }) {
275
288
  const resolved = resolveGenerationModelConfig(options)
276
289
  return {
@@ -1,9 +1,15 @@
1
1
  import assert from 'node:assert/strict'
2
+ import { EventEmitter } from 'node:events'
2
3
  import fs from 'node:fs'
3
4
  import os from 'node:os'
4
5
  import path from 'node:path'
5
6
  import { describe, it } from 'node:test'
6
- import { buildAttachments } from './email'
7
+ import {
8
+ attachImapErrorHandler,
9
+ buildAttachments,
10
+ buildEmailTlsOptions,
11
+ parseTlsRejectUnauthorized,
12
+ } from './email'
7
13
  import { connectorSupportsBinaryMedia } from './response-media'
8
14
 
9
15
  describe('connectorSupportsBinaryMedia — email', () => {
@@ -63,3 +69,30 @@ describe('email buildAttachments', () => {
63
69
  }
64
70
  })
65
71
  })
72
+
73
+ describe('email TLS configuration', () => {
74
+ it('defaults to certificate verification', () => {
75
+ assert.equal(parseTlsRejectUnauthorized(undefined), true)
76
+ assert.equal(parseTlsRejectUnauthorized(''), true)
77
+ assert.deepEqual(buildEmailTlsOptions({ tlsRejectUnauthorized: true }), { rejectUnauthorized: true })
78
+ })
79
+
80
+ it('allows explicit self-signed certificate opt-out', () => {
81
+ assert.equal(parseTlsRejectUnauthorized(false), false)
82
+ assert.equal(parseTlsRejectUnauthorized('false'), false)
83
+ assert.equal(parseTlsRejectUnauthorized('0'), false)
84
+ assert.deepEqual(buildEmailTlsOptions({ tlsRejectUnauthorized: false }), { rejectUnauthorized: false })
85
+ })
86
+
87
+ it('handles IMAP socket errors without leaving the emitter unhandled', () => {
88
+ const imap = new EventEmitter()
89
+ let disconnected = false
90
+
91
+ attachImapErrorHandler(imap, () => {
92
+ disconnected = true
93
+ })
94
+
95
+ assert.doesNotThrow(() => imap.emit('error', new Error('DEPTH_ZERO_SELF_SIGNED_CERT')))
96
+ assert.equal(disconnected, true)
97
+ })
98
+ })
@@ -21,6 +21,7 @@ interface EmailConfig {
21
21
  folder?: string
22
22
  pollIntervalSec?: number
23
23
  subjectPrefix?: string
24
+ tlsRejectUnauthorized: boolean
24
25
  }
25
26
 
26
27
  interface MailAttachment {
@@ -29,6 +30,10 @@ interface MailAttachment {
29
30
  contentType?: string
30
31
  }
31
32
 
33
+ interface ImapErrorEmitter {
34
+ on(event: 'error', listener: (err: unknown) => void): unknown
35
+ }
36
+
32
37
  export function buildAttachments(options?: OutboundSendOptions): MailAttachment[] {
33
38
  const source = options?.mediaPath
34
39
  if (!source) return []
@@ -43,6 +48,28 @@ export function buildAttachments(options?: OutboundSendOptions): MailAttachment[
43
48
  }]
44
49
  }
45
50
 
51
+ export function parseTlsRejectUnauthorized(value: unknown): boolean {
52
+ if (typeof value === 'boolean') return value
53
+ if (typeof value !== 'string') return true
54
+
55
+ const normalized = value.trim().toLowerCase()
56
+ if (!normalized) return true
57
+ if (['false', '0', 'no', 'off', 'disabled'].includes(normalized)) return false
58
+ if (['true', '1', 'yes', 'on', 'enabled'].includes(normalized)) return true
59
+ return true
60
+ }
61
+
62
+ export function buildEmailTlsOptions(config: Pick<EmailConfig, 'tlsRejectUnauthorized'>): { rejectUnauthorized: boolean } {
63
+ return { rejectUnauthorized: config.tlsRejectUnauthorized !== false }
64
+ }
65
+
66
+ export function attachImapErrorHandler(imap: ImapErrorEmitter, onDisconnected: () => void): void {
67
+ imap.on('error', (err: unknown) => {
68
+ onDisconnected()
69
+ log.error(TAG, `IMAP socket error: ${errorMessage(err)}`)
70
+ })
71
+ }
72
+
46
73
  function getConfig(connector: Connector): EmailConfig {
47
74
  const c = connector.config as Record<string, unknown>
48
75
  return {
@@ -55,6 +82,7 @@ function getConfig(connector: Connector): EmailConfig {
55
82
  folder: String(c.folder ?? 'INBOX'),
56
83
  pollIntervalSec: Number(c.pollIntervalSec ?? 60),
57
84
  subjectPrefix: c.subjectPrefix ? String(c.subjectPrefix) : undefined,
85
+ tlsRejectUnauthorized: parseTlsRejectUnauthorized(c.tlsRejectUnauthorized),
58
86
  }
59
87
  }
60
88
 
@@ -68,24 +96,31 @@ const email: PlatformConnector = {
68
96
 
69
97
  const folder = config.folder || 'INBOX'
70
98
  const pollMs = (config.pollIntervalSec || 60) * 1000
99
+ const tls = buildEmailTlsOptions(config)
100
+ let connected = false
71
101
 
72
102
  // IMAP client for inbound
73
103
  const imap = new ImapFlow({
74
104
  host: config.imapHost,
75
105
  port: config.imapPort || 993,
76
106
  secure: (config.imapPort || 993) === 993,
107
+ tls,
77
108
  auth: {
78
109
  user: config.user,
79
110
  pass: config.password,
80
111
  },
81
112
  logger: false,
82
113
  })
114
+ attachImapErrorHandler(imap, () => {
115
+ connected = false
116
+ })
83
117
 
84
118
  // SMTP transport for outbound
85
119
  const smtp: Transporter = createTransport({
86
120
  host: config.smtpHost,
87
121
  port: config.smtpPort || 587,
88
122
  secure: (config.smtpPort || 587) === 465,
123
+ tls,
89
124
  auth: {
90
125
  user: config.user,
91
126
  pass: config.password,
@@ -94,7 +129,6 @@ const email: PlatformConnector = {
94
129
 
95
130
  // Track last seen UID to only process new messages
96
131
  let highwaterUid = 0
97
- let connected = false
98
132
  let pollTimer: ReturnType<typeof setInterval> | null = null
99
133
 
100
134
  // Map to track original sender per channelId (email address) for replies
@@ -0,0 +1,42 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+ import { parseTier2DreamResponseText } from './dream-service'
4
+
5
+ describe('parseTier2DreamResponseText', () => {
6
+ it('parses a plain structured dream response', () => {
7
+ const parsed = parseTier2DreamResponseText(JSON.stringify({
8
+ consolidations: [{
9
+ sourceIds: ['mem-1', 'mem-2'],
10
+ title: 'Shared pattern',
11
+ content: 'Both memories describe the same workflow.',
12
+ }],
13
+ reflections: [{ title: 'Focus', content: 'The agent prefers short release loops.' }],
14
+ flagged: [{ memoryId: 'mem-3', reason: 'Contradicts the current release process.' }],
15
+ }))
16
+
17
+ assert.deepEqual(parsed?.consolidations?.[0]?.sourceIds, ['mem-1', 'mem-2'])
18
+ assert.equal(parsed?.reflections?.[0]?.title, 'Focus')
19
+ assert.equal(parsed?.flagged?.[0]?.memoryId, 'mem-3')
20
+ })
21
+
22
+ it('extracts fenced JSON with nested braces inside strings', () => {
23
+ const parsed = parseTier2DreamResponseText([
24
+ '```json',
25
+ '{',
26
+ ' "consolidations": [{',
27
+ ' "sourceIds": ["mem-1"],',
28
+ ' "title": "Payload shape",',
29
+ ' "content": "The JSON example was {\\"ok\\":true} and should stay intact."',
30
+ ' }]',
31
+ '}',
32
+ '```',
33
+ ].join('\n'))
34
+
35
+ assert.equal(parsed?.consolidations?.[0]?.content, 'The JSON example was {"ok":true} and should stay intact.')
36
+ })
37
+
38
+ it('rejects malformed or schema-incompatible responses', () => {
39
+ assert.equal(parseTier2DreamResponseText('no json here'), null)
40
+ assert.equal(parseTier2DreamResponseText('{"consolidations":[{"sourceIds":[123],"title":"Bad","content":"Bad"}]}'), null)
41
+ })
42
+ })
@@ -1,9 +1,10 @@
1
1
  import crypto from 'crypto'
2
+ import { z } from 'zod'
2
3
  import type { DreamCycle, DreamCycleResult, DreamConfig, DreamTrigger, Agent } from '@/types'
3
4
  import { DEFAULT_DREAM_CONFIG } from '@/types/dream'
4
5
  import { getMemoryDb } from '@/lib/server/memory/memory-db'
5
6
  import { saveDreamCycle } from '@/lib/server/memory/dream-cycles'
6
- import { errorMessage, safeJsonParse } from '@/lib/shared-utils'
7
+ import { errorMessage } from '@/lib/shared-utils'
7
8
  import { log } from '@/lib/server/logger'
8
9
 
9
10
  const TAG = 'dream-service'
@@ -102,6 +103,70 @@ interface Tier2Response {
102
103
  flagged?: Tier2Flagged[]
103
104
  }
104
105
 
106
+ const Tier2ResponseSchema = z.object({
107
+ consolidations: z.array(z.object({
108
+ sourceIds: z.array(z.string()),
109
+ title: z.string(),
110
+ content: z.string(),
111
+ })).optional(),
112
+ reflections: z.array(z.object({
113
+ title: z.string(),
114
+ content: z.string(),
115
+ })).optional(),
116
+ flagged: z.array(z.object({
117
+ memoryId: z.string(),
118
+ reason: z.string(),
119
+ })).optional(),
120
+ }).passthrough()
121
+
122
+ function findBalancedJsonObjectEnd(text: string, start: number): number {
123
+ let depth = 0
124
+ let inString = false
125
+ let escaped = false
126
+
127
+ for (let index = start; index < text.length; index += 1) {
128
+ const char = text[index]
129
+ if (inString) {
130
+ if (escaped) escaped = false
131
+ else if (char === '\\') escaped = true
132
+ else if (char === '"') inString = false
133
+ continue
134
+ }
135
+ if (char === '"') {
136
+ inString = true
137
+ continue
138
+ }
139
+ if (char === '{') depth += 1
140
+ else if (char === '}') depth -= 1
141
+ if (depth === 0) return index + 1
142
+ }
143
+ return -1
144
+ }
145
+
146
+ function extractFirstBalancedJsonObject(text: string): string | null {
147
+ const source = String(text || '')
148
+ for (let index = 0; index < source.length; index += 1) {
149
+ if (source[index] !== '{') continue
150
+ const end = findBalancedJsonObjectEnd(source, index)
151
+ if (end === -1) return null
152
+ return source.slice(index, end)
153
+ }
154
+ return null
155
+ }
156
+
157
+ export function parseTier2DreamResponseText(text: string): Tier2Response | null {
158
+ const jsonText = extractFirstBalancedJsonObject(text)
159
+ if (!jsonText) return null
160
+ let raw: unknown
161
+ try {
162
+ raw = JSON.parse(jsonText)
163
+ } catch {
164
+ return null
165
+ }
166
+ const parsed = Tier2ResponseSchema.safeParse(raw)
167
+ return parsed.success ? parsed.data : null
168
+ }
169
+
105
170
  export async function runTier2Dream(
106
171
  agentId: string,
107
172
  config: DreamConfig,
@@ -149,7 +214,7 @@ ${memoryLines.join('\n')}`
149
214
 
150
215
  try {
151
216
  const { buildLLM } = await import('@/lib/server/build-llm')
152
- const { llm } = await buildLLM({ agentId })
217
+ const { llm } = await buildLLM({ agentId, responseFormat: 'json_object' })
153
218
  const { HumanMessage } = await import('@langchain/core/messages')
154
219
 
155
220
  const response = await llm.invoke([new HumanMessage(prompt)])
@@ -159,9 +224,10 @@ ${memoryLines.join('\n')}`
159
224
  ? response.content.map((b) => ('text' in b && typeof b.text === 'string' ? b.text : '')).join('')
160
225
  : ''
161
226
 
162
- // Extract JSON from response (handle markdown code blocks)
163
- const jsonMatch = text.match(/\{[\s\S]*\}/)
164
- const parsed = safeJsonParse<Tier2Response>(jsonMatch?.[0] ?? '', {})
227
+ const parsed = parseTier2DreamResponseText(text) ?? {}
228
+ if (!parsed.consolidations && !parsed.reflections && !parsed.flagged) {
229
+ errors.push('Tier 2 dream response was not valid structured JSON.')
230
+ }
165
231
 
166
232
  // Process consolidations
167
233
  if (Array.isArray(parsed.consolidations)) {
@@ -187,6 +187,10 @@ const COLLECTIONS = [
187
187
 
188
188
  export type StorageCollection = (typeof COLLECTIONS)[number]
189
189
 
190
+ const ARRAY_VALUE_COLLECTIONS = new Set<StorageCollection>([
191
+ 'model_overrides',
192
+ ])
193
+
190
194
  for (const table of COLLECTIONS) {
191
195
  db.exec(`CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, data TEXT NOT NULL)`)
192
196
  }
@@ -246,18 +250,20 @@ function getCollectionRawCache(table: string): LRUMap<string, string> {
246
250
  }
247
251
 
248
252
  function loadCollectionWithNormalizationState(table: string): {
249
- result: Record<string, StoredObject>
253
+ result: Record<string, StoredObject | unknown[]>
250
254
  normalizedCount: number
251
255
  } {
252
256
  const endPerf = perf.start('storage', 'loadCollection', { table })
253
257
  const raw = getCollectionRawCache(table)
254
- const result: Record<string, StoredObject> = {}
258
+ const result: Record<string, StoredObject | unknown[]> = {}
259
+ const allowsArrayValues = ARRAY_VALUE_COLLECTIONS.has(table as StorageCollection)
255
260
  let normalizedCount = 0
256
261
  for (const [id, data] of raw.entries()) {
257
262
  try {
258
263
  const { value: normalized, changed } = normalize(table, JSON.parse(data))
259
- if (!normalized || typeof normalized !== 'object' || Array.isArray(normalized)) continue
260
- result[id] = normalized as StoredObject
264
+ if (!normalized || typeof normalized !== 'object') continue
265
+ if (Array.isArray(normalized) && !allowsArrayValues) continue
266
+ result[id] = normalized as StoredObject | unknown[]
261
267
  if (changed) normalizedCount += 1
262
268
  } catch (err) {
263
269
  const fingerprint = `${table}:${id}`
@@ -277,8 +283,8 @@ function loadCollectionWithNormalizationState(table: string): {
277
283
 
278
284
  export function loadCollection(table: string): Record<string, StoredObject> {
279
285
  const { result, normalizedCount } = loadCollectionWithNormalizationState(table)
280
- if (normalizedCount > 0) saveCollection(table, result)
281
- return result
286
+ if (normalizedCount > 0) saveCollection(table, result as Record<string, unknown>)
287
+ return result as Record<string, StoredObject>
282
288
  }
283
289
 
284
290
  function saveCollection(table: string, data: Record<string, unknown>) {
@@ -1001,7 +1007,7 @@ export function patchAgent(
1001
1007
  const schedulesStore = createCollectionStore('schedules', { ttlMs: 10_000 })
1002
1008
  export function loadSchedules(): Record<string, Schedule> {
1003
1009
  const { result, normalizedCount } = loadCollectionWithNormalizationState('schedules')
1004
- if (normalizedCount > 0) saveCollection('schedules', result)
1010
+ if (normalizedCount > 0) saveCollection('schedules', result as Record<string, unknown>)
1005
1011
  return result as unknown as Record<string, Schedule>
1006
1012
  }
1007
1013
  export const saveSchedules = schedulesStore.save
@@ -1,218 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- const electron_1 = require("electron");
40
- const node_fs_1 = __importDefault(require("node:fs"));
41
- const node_path_1 = __importDefault(require("node:path"));
42
- const paths_1 = require("./paths");
43
- const server_lifecycle_1 = require("./server-lifecycle");
44
- const menu_1 = require("./menu");
45
- const DEV_URL_DEFAULT = 'http://127.0.0.1:3456';
46
- const LOG_TAIL_BYTES = 1500;
47
- let mainWindow = null;
48
- let serverHandle = null;
49
- let serverLogFile = null;
50
- let isQuitting = false;
51
- const gotLock = electron_1.app.requestSingleInstanceLock();
52
- if (!gotLock) {
53
- electron_1.app.quit();
54
- }
55
- else {
56
- electron_1.app.on('second-instance', () => {
57
- if (mainWindow) {
58
- if (mainWindow.isMinimized())
59
- mainWindow.restore();
60
- mainWindow.focus();
61
- }
62
- });
63
- electron_1.app.on('ready', () => void onReady());
64
- electron_1.app.on('window-all-closed', () => {
65
- if (process.platform !== 'darwin')
66
- electron_1.app.quit();
67
- });
68
- electron_1.app.on('activate', () => {
69
- if (mainWindow !== null)
70
- return;
71
- if (serverHandle) {
72
- createMainWindow(serverHandle.url);
73
- }
74
- else if (!electron_1.app.isPackaged) {
75
- createMainWindow(process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT);
76
- }
77
- });
78
- electron_1.app.on('before-quit', () => {
79
- isQuitting = true;
80
- });
81
- electron_1.app.on('will-quit', async (event) => {
82
- if (!serverHandle)
83
- return;
84
- event.preventDefault();
85
- try {
86
- await serverHandle.stop();
87
- }
88
- finally {
89
- serverHandle = null;
90
- electron_1.app.exit(0);
91
- }
92
- });
93
- }
94
- async function onReady() {
95
- const paths = (0, paths_1.resolveRuntimePaths)();
96
- (0, menu_1.buildAppMenu)(paths, () => mainWindow);
97
- const iconPath = resolveIconPath();
98
- if (process.platform === 'darwin' && iconPath && electron_1.app.dock) {
99
- const img = electron_1.nativeImage.createFromPath(iconPath);
100
- if (!img.isEmpty())
101
- electron_1.app.dock.setIcon(img);
102
- }
103
- if (!electron_1.app.isPackaged) {
104
- const devUrl = process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT;
105
- console.log(`[swarmclaw] dev mode, loading ${devUrl}`);
106
- createMainWindow(devUrl);
107
- return;
108
- }
109
- serverLogFile = node_path_1.default.join(electron_1.app.getPath('userData'), 'logs', 'server.log');
110
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(serverLogFile), { recursive: true });
111
- try {
112
- serverHandle = await (0, server_lifecycle_1.startEmbeddedServer)({
113
- paths,
114
- logFile: serverLogFile,
115
- onStdout: (c) => process.stdout.write(`[swarmclaw] ${c}`),
116
- onStderr: (c) => process.stderr.write(`[swarmclaw] ${c}`),
117
- onExit: (code, signal) => {
118
- if (!isQuitting) {
119
- console.error(`[swarmclaw] server exited unexpectedly (code=${code}, signal=${signal ?? 'none'})`);
120
- void showServerCrashDialog(code, signal);
121
- }
122
- },
123
- });
124
- }
125
- catch (err) {
126
- await showStartupFailureDialog(err, paths);
127
- electron_1.app.exit(1);
128
- return;
129
- }
130
- createMainWindow(serverHandle.url);
131
- void Promise.resolve().then(() => __importStar(require('./updater'))).then((m) => m.initAutoUpdater());
132
- }
133
- function resolveIconPath() {
134
- const candidate = electron_1.app.isPackaged
135
- ? node_path_1.default.join(process.resourcesPath, 'icon.png')
136
- : node_path_1.default.join(__dirname, '..', 'resources', 'icon.png');
137
- return node_fs_1.default.existsSync(candidate) ? candidate : undefined;
138
- }
139
- function createMainWindow(startUrl) {
140
- const iconPath = resolveIconPath();
141
- mainWindow = new electron_1.BrowserWindow({
142
- width: 1440,
143
- height: 900,
144
- minWidth: 1024,
145
- minHeight: 640,
146
- backgroundColor: '#0b0b0f',
147
- show: true,
148
- ...(iconPath ? { icon: iconPath } : {}),
149
- webPreferences: {
150
- contextIsolation: true,
151
- nodeIntegration: false,
152
- sandbox: false,
153
- },
154
- });
155
- const wc = mainWindow.webContents;
156
- if (!electron_1.app.isPackaged)
157
- wc.openDevTools({ mode: 'detach' });
158
- wc.on('did-start-loading', () => console.log('[swarmclaw] did-start-loading'));
159
- wc.on('did-finish-load', () => console.log('[swarmclaw] did-finish-load'));
160
- wc.on('did-fail-load', (_e, code, desc, url) => console.error(`[swarmclaw] did-fail-load code=${code} desc=${desc} url=${url}`));
161
- wc.on('render-process-gone', (_e, details) => console.error(`[swarmclaw] render-process-gone reason=${details.reason}`));
162
- wc.on('unresponsive', () => console.error('[swarmclaw] webContents unresponsive'));
163
- mainWindow.on('closed', () => {
164
- mainWindow = null;
165
- });
166
- mainWindow.webContents.setWindowOpenHandler(({ url }) => {
167
- if (url.startsWith(startUrl))
168
- return { action: 'allow' };
169
- void electron_1.shell.openExternal(url);
170
- return { action: 'deny' };
171
- });
172
- void mainWindow.loadURL(startUrl).catch((err) => {
173
- console.error('[swarmclaw] loadURL rejected:', err);
174
- });
175
- }
176
- async function showServerCrashDialog(code, signal) {
177
- const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
178
- const quitButtonId = buttons.length - 1;
179
- const detail = buildLogDetail(`code=${code ?? 'null'} signal=${signal ?? 'none'}`);
180
- const res = await electron_1.dialog.showMessageBox({
181
- type: 'error',
182
- buttons,
183
- defaultId: quitButtonId,
184
- cancelId: quitButtonId,
185
- title: 'SwarmClaw stopped',
186
- message: 'The SwarmClaw server exited unexpectedly.',
187
- detail,
188
- });
189
- if (serverLogFile && res.response === 0)
190
- electron_1.shell.showItemInFolder(serverLogFile);
191
- electron_1.app.exit(1);
192
- }
193
- async function showStartupFailureDialog(err, paths) {
194
- const message = err instanceof Error ? err.message : String(err);
195
- const base = `${message}\n\nStandalone entry: ${paths.standaloneEntry}\nData dir: ${paths.dataDir}`;
196
- const detail = buildLogDetail(base);
197
- const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
198
- const quitButtonId = buttons.length - 1;
199
- const res = await electron_1.dialog.showMessageBox({
200
- type: 'error',
201
- buttons,
202
- defaultId: quitButtonId,
203
- cancelId: quitButtonId,
204
- title: 'SwarmClaw failed to start',
205
- message: 'The embedded server did not start.',
206
- detail,
207
- });
208
- if (serverLogFile && res.response === 0)
209
- electron_1.shell.showItemInFolder(serverLogFile);
210
- }
211
- function buildLogDetail(base) {
212
- if (!serverLogFile)
213
- return base;
214
- const tail = (0, server_lifecycle_1.tailLogFile)(serverLogFile, LOG_TAIL_BYTES).trim();
215
- if (!tail)
216
- return `${base}\n\nLog file: ${serverLogFile}\n(no output captured yet)`;
217
- return `${base}\n\nLog tail (${serverLogFile}):\n${tail}`;
218
- }