@swarmclawai/swarmclaw 1.9.29 → 1.9.31

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,6 +151,21 @@ clawhub install swarmclaw
151
151
 
152
152
  [Browse on ClawHub](https://clawhub.ai/skills/swarmclaw)
153
153
 
154
+ ## v1.9.31 Highlights
155
+
156
+ Documentation cleanup release for public release notes and OpenClaw guidance. No runtime behavior changed.
157
+
158
+ - **Public docs cleanup.** Removed an unwanted third-party example from the README and site release notes.
159
+ - **OpenClaw guidance preserved.** The README keeps the SwarmClaw-native OpenClaw gateway, skill, and agent-file guidance without naming unrelated workflows.
160
+
161
+ ## v1.9.30 Highlights
162
+
163
+ PR integration release for dream-model routing, email bridge TLS opt-outs, and installed CLI runtime resolution.
164
+
165
+ - **Dream model routing.** Memory dream cycles and daily digests can use optional `dreamProvider` settings so background consolidation can run on a smaller local model.
166
+ - **Email bridge TLS opt-outs.** `tlsRejectUnauthorized=false` now disables hostname checks too, matching the explicit self-signed-server opt-out.
167
+ - **Installed CLI stability.** Legacy API-backed CLI commands import the package-local `tsx` runtime instead of resolving `tsx` from the caller's project.
168
+
154
169
  ## v1.9.29 Highlights
155
170
 
156
171
  Issue-fix release for Edit Agent tooltips, installed package builds, and structured dream output on local Ollama models.
@@ -219,6 +234,7 @@ SwarmClaw is built for OpenClaw operators who need more than one agent or one ga
219
234
  - Deploy official-image OpenClaw runtimes locally, via VPS bundles, or over SSH.
220
235
  - Edit OpenClaw agent files such as `SOUL.md`, `IDENTITY.md`, `USER.md`, `TOOLS.md`, and `AGENTS.md`.
221
236
  - Import OpenClaw `SKILL.md` files and use them in SwarmClaw's runtime skill system.
237
+ - Use OpenClaw plugins and skills through the configured gateway workflow without leaving the SwarmClaw control plane.
222
238
 
223
239
  ## Use Cases
224
240
 
@@ -410,6 +426,21 @@ Operational docs: https://swarmclaw.ai/docs/observability
410
426
 
411
427
  ## Releases
412
428
 
429
+ ### v1.9.31 Highlights
430
+
431
+ Documentation cleanup release for public release notes and OpenClaw guidance. No runtime behavior changed.
432
+
433
+ - **Public docs cleanup.** Removed an unwanted third-party example from the README and site release notes.
434
+ - **OpenClaw guidance preserved.** The README keeps the SwarmClaw-native OpenClaw gateway, skill, and agent-file guidance without naming unrelated workflows.
435
+
436
+ ### v1.9.30 Highlights
437
+
438
+ PR integration release for dream-model routing, email bridge TLS opt-outs, and installed CLI runtime resolution.
439
+
440
+ - **Dream model routing.** Memory dream cycles and daily digests can use optional `dreamProvider` settings so background consolidation can run on a smaller local model.
441
+ - **Email bridge TLS opt-outs.** `tlsRejectUnauthorized=false` now disables hostname checks too, matching the explicit self-signed-server opt-out.
442
+ - **Installed CLI stability.** Legacy API-backed CLI commands import the package-local `tsx` runtime instead of resolving `tsx` from the caller's project.
443
+
413
444
  ### v1.9.29 Highlights
414
445
 
415
446
  Issue-fix release for Edit Agent tooltips, installed package builds, and structured dream output on local Ollama models.
package/bin/swarmclaw.js CHANGED
@@ -47,11 +47,14 @@ function supportsStripTypes() {
47
47
  }
48
48
 
49
49
  function hasTsxRuntime() {
50
+ return Boolean(resolveTsxRuntimeImportPath())
51
+ }
52
+
53
+ function resolveTsxRuntimeImportPath() {
50
54
  try {
51
- require.resolve('tsx/package.json')
52
- return true
55
+ return require.resolve('tsx')
53
56
  } catch {
54
- return false
57
+ return null
55
58
  }
56
59
  }
57
60
 
@@ -71,9 +74,10 @@ function buildLegacyTsCliArgs(cliPath, argv, options = {}) {
71
74
  return ['--no-warnings', '--experimental-strip-types', cliPath, ...argv]
72
75
  }
73
76
 
74
- const tsxAvailable = options.hasTsxRuntime ?? hasTsxRuntime()
77
+ const tsxImportPath = options.tsxImportPath ?? resolveTsxRuntimeImportPath()
78
+ const tsxAvailable = options.hasTsxRuntime ?? Boolean(tsxImportPath)
75
79
  if (tsxAvailable) {
76
- return ['--no-warnings', '--import', 'tsx', cliPath, ...argv]
80
+ return ['--no-warnings', '--import', tsxImportPath || 'tsx', cliPath, ...argv]
77
81
  }
78
82
 
79
83
  return null
@@ -374,6 +378,7 @@ module.exports = {
374
378
  normalizeLegacyTsCliArgv,
375
379
  pathIsInsideNodeModules,
376
380
  resolveLegacyTsCliPath,
381
+ resolveTsxRuntimeImportPath,
377
382
  TS_CLI_ACTIONS,
378
383
  normalizeLegacyCliEnv,
379
384
  printPackageVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.9.29",
3
+ "version": "1.9.31",
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",
@@ -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, resolveLegacyTsCliPath } = require('../../bin/swarmclaw.js')
10
+ const { buildLegacyTsCliArgs, resolveLegacyTsCliPath, resolveTsxRuntimeImportPath } = 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')
@@ -207,21 +207,30 @@ test('package ships dagre type declarations required by installed builds', () =>
207
207
 
208
208
  test('legacy TS launcher falls back to tsx import when strip-types is unavailable', () => {
209
209
  const cliPath = path.join(APP_ROOT, 'src', 'cli', 'index.ts')
210
+ const tsxImportPath = resolveTsxRuntimeImportPath()
210
211
  const args = buildLegacyTsCliArgs(cliPath, ['runs', 'list'], {
211
212
  supportsStripTypes: false,
212
213
  hasTsxRuntime: true,
213
214
  })
214
215
 
215
- assert.deepEqual(args, ['--no-warnings', '--import', 'tsx', cliPath, 'runs', 'list'])
216
+ assert.deepEqual(args, ['--no-warnings', '--import', tsxImportPath, cliPath, 'runs', 'list'])
216
217
  })
217
218
 
218
219
  test('legacy TS launcher uses tsx instead of strip-types inside node_modules', () => {
219
220
  const cliPath = path.join(os.tmpdir(), 'node_modules', '@swarmclawai', 'swarmclaw', 'src', 'cli', 'index.ts')
221
+ const tsxImportPath = resolveTsxRuntimeImportPath()
220
222
  const args = buildLegacyTsCliArgs(cliPath, ['agents', 'list'], {
221
223
  supportsStripTypes: true,
222
224
  hasTsxRuntime: true,
223
225
  })
224
226
 
225
- assert.deepEqual(args, ['--no-warnings', '--import', 'tsx', cliPath, 'agents', 'list'])
227
+ assert.deepEqual(args, ['--no-warnings', '--import', tsxImportPath, cliPath, 'agents', 'list'])
226
228
  assert.equal(resolveLegacyTsCliPath(), path.join(APP_ROOT, 'src', 'cli', 'index.ts'))
227
229
  })
230
+
231
+ test('legacy TS launcher imports the package-local tsx runtime by absolute path', () => {
232
+ const tsxImportPath = resolveTsxRuntimeImportPath()
233
+
234
+ assert.equal(path.isAbsolute(tsxImportPath), true)
235
+ assert.match(tsxImportPath, /tsx/)
236
+ })
@@ -81,7 +81,10 @@ describe('email TLS configuration', () => {
81
81
  assert.equal(parseTlsRejectUnauthorized(false), false)
82
82
  assert.equal(parseTlsRejectUnauthorized('false'), false)
83
83
  assert.equal(parseTlsRejectUnauthorized('0'), false)
84
- assert.deepEqual(buildEmailTlsOptions({ tlsRejectUnauthorized: false }), { rejectUnauthorized: false })
84
+ const tls = buildEmailTlsOptions({ tlsRejectUnauthorized: false })
85
+ assert.equal(tls.rejectUnauthorized, false)
86
+ assert.equal(typeof tls.checkServerIdentity, 'function')
87
+ assert.equal(tls.checkServerIdentity?.('localhost', {} as never), undefined)
85
88
  })
86
89
 
87
90
  it('handles IMAP socket errors without leaving the emitter unhandled', () => {
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
+ import type { ConnectionOptions } from 'node:tls'
3
4
  import { ImapFlow } from 'imapflow'
4
5
  import { createTransport, type Transporter } from 'nodemailer'
5
6
  import { simpleParser } from 'mailparser'
@@ -34,6 +35,10 @@ interface ImapErrorEmitter {
34
35
  on(event: 'error', listener: (err: unknown) => void): unknown
35
36
  }
36
37
 
38
+ type EmailTlsOptions = Pick<ConnectionOptions, 'rejectUnauthorized' | 'checkServerIdentity'> & {
39
+ rejectUnauthorized: boolean
40
+ }
41
+
37
42
  export function buildAttachments(options?: OutboundSendOptions): MailAttachment[] {
38
43
  const source = options?.mediaPath
39
44
  if (!source) return []
@@ -59,8 +64,11 @@ export function parseTlsRejectUnauthorized(value: unknown): boolean {
59
64
  return true
60
65
  }
61
66
 
62
- export function buildEmailTlsOptions(config: Pick<EmailConfig, 'tlsRejectUnauthorized'>): { rejectUnauthorized: boolean } {
63
- return { rejectUnauthorized: config.tlsRejectUnauthorized !== false }
67
+ export function buildEmailTlsOptions(config: Pick<EmailConfig, 'tlsRejectUnauthorized'>): EmailTlsOptions {
68
+ const reject = config.tlsRejectUnauthorized !== false
69
+ return reject
70
+ ? { rejectUnauthorized: true }
71
+ : { rejectUnauthorized: false, checkServerIdentity: () => undefined }
64
72
  }
65
73
 
66
74
  export function attachImapErrorHandler(imap: ImapErrorEmitter, onDisconnected: () => void): void {
@@ -0,0 +1,22 @@
1
+ import type { GenerationModelPreference } from '@/lib/server/build-llm'
2
+ import type { AppSettings } from '@/types'
3
+
4
+ type DreamGenerationSettings = Pick<AppSettings, 'dreamProvider' | 'dreamModel' | 'dreamCredentialId' | 'dreamEndpoint'> | Record<string, unknown> | null | undefined
5
+
6
+ function optionalSettingString(value: unknown): string | undefined {
7
+ const normalized = typeof value === 'string' ? value.trim() : ''
8
+ return normalized || undefined
9
+ }
10
+
11
+ export function resolveDreamGenerationPreference(settings: DreamGenerationSettings): GenerationModelPreference | undefined {
12
+ const record = (settings || {}) as Record<string, unknown>
13
+ const provider = optionalSettingString(record.dreamProvider)
14
+ if (!provider) return undefined
15
+
16
+ return {
17
+ provider,
18
+ model: optionalSettingString(record.dreamModel),
19
+ credentialId: optionalSettingString(record.dreamCredentialId),
20
+ apiEndpoint: optionalSettingString(record.dreamEndpoint),
21
+ }
22
+ }
@@ -1,7 +1,29 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { describe, it } from 'node:test'
3
+ import { resolveDreamGenerationPreference } from './dream-generation-preference'
3
4
  import { parseTier2DreamResponseText } from './dream-service'
4
5
 
6
+ describe('resolveDreamGenerationPreference', () => {
7
+ it('returns no preference when no dream provider is configured', () => {
8
+ assert.equal(resolveDreamGenerationPreference({}), undefined)
9
+ assert.equal(resolveDreamGenerationPreference({ dreamProvider: ' ' }), undefined)
10
+ })
11
+
12
+ it('builds a trimmed dream model preference from app settings', () => {
13
+ assert.deepEqual(resolveDreamGenerationPreference({
14
+ dreamProvider: ' ollama ',
15
+ dreamModel: ' gemma4:e4b ',
16
+ dreamCredentialId: ' cred-1 ',
17
+ dreamEndpoint: ' http://localhost:11434 ',
18
+ }), {
19
+ provider: 'ollama',
20
+ model: 'gemma4:e4b',
21
+ credentialId: 'cred-1',
22
+ apiEndpoint: 'http://localhost:11434',
23
+ })
24
+ })
25
+ })
26
+
5
27
  describe('parseTier2DreamResponseText', () => {
6
28
  it('parses a plain structured dream response', () => {
7
29
  const parsed = parseTier2DreamResponseText(JSON.stringify({
@@ -6,6 +6,7 @@ import { getMemoryDb } from '@/lib/server/memory/memory-db'
6
6
  import { saveDreamCycle } from '@/lib/server/memory/dream-cycles'
7
7
  import { errorMessage } from '@/lib/shared-utils'
8
8
  import { log } from '@/lib/server/logger'
9
+ import { resolveDreamGenerationPreference } from '@/lib/server/memory/dream-generation-preference'
9
10
 
10
11
  const TAG = 'dream-service'
11
12
 
@@ -214,7 +215,9 @@ ${memoryLines.join('\n')}`
214
215
 
215
216
  try {
216
217
  const { buildLLM } = await import('@/lib/server/build-llm')
217
- const { llm } = await buildLLM({ agentId, responseFormat: 'json_object' })
218
+ const { loadSettings } = await import('@/lib/server/settings/settings-repository')
219
+ const preferred = resolveDreamGenerationPreference(loadSettings())
220
+ const { llm } = await buildLLM({ agentId, preferred, responseFormat: 'json_object' })
218
221
  const { HumanMessage } = await import('@langchain/core/messages')
219
222
 
220
223
  const response = await llm.invoke([new HumanMessage(prompt)])
@@ -38,6 +38,7 @@ after(() => {
38
38
  })
39
39
 
40
40
  test('runDailyConsolidation skips orphaned and CLI-only agent namespaces without reporting errors', async () => {
41
+ storage.saveSettings({})
41
42
  const db = memDb.getMemoryDb()
42
43
  const now = Date.now()
43
44
  const orphanId = 'live-orphan-agent'
@@ -87,3 +88,39 @@ test('runDailyConsolidation skips orphaned and CLI-only agent namespaces without
87
88
  false,
88
89
  )
89
90
  })
91
+
92
+ test('canCreateDailyDigestForAgent allows CLI-only agents when a dream model is configured', async () => {
93
+ const now = Date.now()
94
+ const agentId = 'dream-routed-cli-agent'
95
+ storage.saveAgents({
96
+ [agentId]: {
97
+ id: agentId,
98
+ name: 'Dream Routed CLI Agent',
99
+ description: '',
100
+ systemPrompt: '',
101
+ provider: 'claude-cli',
102
+ model: 'claude-sonnet-4-5',
103
+ credentialId: null,
104
+ fallbackCredentialIds: [],
105
+ apiEndpoint: null,
106
+ createdAt: now,
107
+ updatedAt: now,
108
+ } as Agent,
109
+ })
110
+
111
+ storage.saveSettings({})
112
+ assert.equal(
113
+ consolidation.canCreateDailyDigestForAgent(agentId, storage.loadAgents({ includeTrashed: true }), storage.loadSettings()),
114
+ false,
115
+ )
116
+
117
+ storage.saveSettings({
118
+ dreamProvider: 'ollama',
119
+ dreamModel: 'llama3.2',
120
+ dreamEndpoint: 'http://127.0.0.1:11434',
121
+ })
122
+ assert.equal(
123
+ consolidation.canCreateDailyDigestForAgent(agentId, storage.loadAgents({ includeTrashed: true }), storage.loadSettings()),
124
+ true,
125
+ )
126
+ })
@@ -4,6 +4,9 @@ import { resolveGenerationModelConfig } from '@/lib/server/build-llm'
4
4
  import { HumanMessage } from '@langchain/core/messages'
5
5
  import { errorMessage } from '@/lib/shared-utils'
6
6
  import { onNextIdleWindow } from '@/lib/server/runtime/idle-window'
7
+ import { loadSettings } from '@/lib/server/settings/settings-repository'
8
+ import { resolveDreamGenerationPreference } from '@/lib/server/memory/dream-generation-preference'
9
+ import type { AppSettings } from '@/types'
7
10
 
8
11
  let consolidationRegistered = false
9
12
  let compactionRegistered = false
@@ -35,14 +38,18 @@ export function registerCompactionIdleCallback(): void {
35
38
  })
36
39
  }
37
40
 
38
- function canCreateDailyDigestForAgent(
41
+ export function canCreateDailyDigestForAgent(
39
42
  agentId: string,
40
43
  agents: ReturnType<typeof loadAgents>,
44
+ settings: Partial<AppSettings> | Record<string, unknown> | null | undefined = loadSettings(),
41
45
  ): boolean {
42
46
  const agent = agents[agentId]
43
47
  if (!agent || agent.trashedAt) return false
44
48
  try {
45
- resolveGenerationModelConfig({ agentId })
49
+ resolveGenerationModelConfig({
50
+ agentId,
51
+ preferred: resolveDreamGenerationPreference(settings),
52
+ })
46
53
  return true
47
54
  } catch (err: unknown) {
48
55
  const message = errorMessage(err)
@@ -65,6 +72,7 @@ export async function runDailyConsolidation(): Promise<{
65
72
  const memDb = getMemoryDb()
66
73
  const counts = memDb.countsByAgent()
67
74
  const agents = loadAgents({ includeTrashed: true })
75
+ const settings = loadSettings()
68
76
  const today = new Date().toISOString().slice(0, 10) // YYYY-MM-DD
69
77
  const digestTitle = `Daily digest: ${today}`
70
78
  const cutoff24h = Date.now() - 24 * 3600_000
@@ -76,7 +84,7 @@ export async function runDailyConsolidation(): Promise<{
76
84
  const agentId = agentKey
77
85
 
78
86
  try {
79
- if (!canCreateDailyDigestForAgent(agentId, agents)) continue
87
+ if (!canCreateDailyDigestForAgent(agentId, agents, settings)) continue
80
88
 
81
89
  // Check if digest already exists for today
82
90
  const existing = memDb.search(digestTitle, agentId)
@@ -109,9 +117,12 @@ export async function runDailyConsolidation(): Promise<{
109
117
  ...memoryLines,
110
118
  ].join('\n')
111
119
 
112
- // Use the target agent's configured generation provider
120
+ // Use an optional dream-model override before the target agent's generation provider.
113
121
  const { buildLLM } = await import('@/lib/server/build-llm')
114
- const { llm } = await buildLLM({ agentId })
122
+ const { llm } = await buildLLM({
123
+ agentId,
124
+ preferred: resolveDreamGenerationPreference(settings),
125
+ })
115
126
 
116
127
  const response = await llm.invoke([new HumanMessage(prompt)])
117
128
  const digestContent = typeof response.content === 'string'
@@ -27,6 +27,11 @@ export interface AppSettings {
27
27
  embeddingModel?: string | null
28
28
  embeddingCredentialId?: string | null
29
29
  embeddingEndpoint?: string | null
30
+ // Optional model override for memory consolidation and dream cycles.
31
+ dreamProvider?: string | null
32
+ dreamModel?: string | null
33
+ dreamCredentialId?: string | null
34
+ dreamEndpoint?: string | null
30
35
  loopMode?: LoopMode
31
36
  agentLoopRecursionLimit?: number
32
37
  delegationMaxDepth?: number