Skip to content

Commit 80d8f6b

Browse files
committed
fix: integrate open PR fixes for v1.9.30
1 parent 9fa1550 commit 80d8f6b

13 files changed

Lines changed: 164 additions & 20 deletions

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,15 @@ clawhub install swarmclaw
151151

152152
[Browse on ClawHub](https://clawhub.ai/skills/swarmclaw)
153153

154+
## v1.9.30 Highlights
155+
156+
PR integration release for dream-model routing, email bridge TLS opt-outs, installed CLI runtime resolution, and an OpenClaw plugin workflow example.
157+
158+
- **Dream model routing.** Memory dream cycles and daily digests can use optional `dreamProvider` settings so background consolidation can run on a smaller local model.
159+
- **Email bridge TLS opt-outs.** `tlsRejectUnauthorized=false` now disables hostname checks too, matching the explicit self-signed-server opt-out.
160+
- **Installed CLI stability.** Legacy API-backed CLI commands import the package-local `tsx` runtime instead of resolving `tsx` from the caller's project.
161+
- **OpenClaw plugin workflow.** README guidance now includes a concrete TweetClaw plugin workflow for OpenClaw operators.
162+
154163
## v1.9.29 Highlights
155164

156165
Issue-fix release for Edit Agent tooltips, installed package builds, and structured dream output on local Ollama models.
@@ -219,6 +228,7 @@ SwarmClaw is built for OpenClaw operators who need more than one agent or one ga
219228
- Deploy official-image OpenClaw runtimes locally, via VPS bundles, or over SSH.
220229
- Edit OpenClaw agent files such as `SOUL.md`, `IDENTITY.md`, `USER.md`, `TOOLS.md`, and `AGENTS.md`.
221230
- Import OpenClaw `SKILL.md` files and use them in SwarmClaw's runtime skill system.
231+
- Use OpenClaw plugins for domain workflows. For example, `openclaw plugins install @xquik/tweetclaw` installs [TweetClaw](https://github.com/Xquik-dev/tweetclaw) via [ClawHub](https://clawhub.ai/kriptoburak/xquik-tweetclaw) for X/Twitter search, follower export, monitors, webhooks, and approval-gated post/reply actions.
222232

223233
## Use Cases
224234

@@ -410,6 +420,15 @@ Operational docs: https://swarmclaw.ai/docs/observability
410420

411421
## Releases
412422

423+
### v1.9.30 Highlights
424+
425+
PR integration release for dream-model routing, email bridge TLS opt-outs, installed CLI runtime resolution, and an OpenClaw plugin workflow example.
426+
427+
- **Dream model routing.** Memory dream cycles and daily digests can use optional `dreamProvider` settings so background consolidation can run on a smaller local model.
428+
- **Email bridge TLS opt-outs.** `tlsRejectUnauthorized=false` now disables hostname checks too, matching the explicit self-signed-server opt-out.
429+
- **Installed CLI stability.** Legacy API-backed CLI commands import the package-local `tsx` runtime instead of resolving `tsx` from the caller's project.
430+
- **OpenClaw plugin workflow.** README guidance now includes a concrete TweetClaw plugin workflow for OpenClaw operators.
431+
413432
### v1.9.29 Highlights
414433

415434
Issue-fix release for Edit Agent tooltips, installed package builds, and structured dream output on local Ollama models.

bin/swarmclaw.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ function supportsStripTypes() {
4747
}
4848

4949
function hasTsxRuntime() {
50+
return Boolean(resolveTsxRuntimeImportPath())
51+
}
52+
53+
function resolveTsxRuntimeImportPath() {
5054
try {
51-
require.resolve('tsx/package.json')
52-
return true
55+
return require.resolve('tsx')
5356
} catch {
54-
return false
57+
return null
5558
}
5659
}
5760

@@ -71,9 +74,10 @@ function buildLegacyTsCliArgs(cliPath, argv, options = {}) {
7174
return ['--no-warnings', '--experimental-strip-types', cliPath, ...argv]
7275
}
7376

74-
const tsxAvailable = options.hasTsxRuntime ?? hasTsxRuntime()
77+
const tsxImportPath = options.tsxImportPath ?? resolveTsxRuntimeImportPath()
78+
const tsxAvailable = options.hasTsxRuntime ?? Boolean(tsxImportPath)
7579
if (tsxAvailable) {
76-
return ['--no-warnings', '--import', 'tsx', cliPath, ...argv]
80+
return ['--no-warnings', '--import', tsxImportPath || 'tsx', cliPath, ...argv]
7781
}
7882

7983
return null
@@ -374,6 +378,7 @@ module.exports = {
374378
normalizeLegacyTsCliArgv,
375379
pathIsInsideNodeModules,
376380
resolveLegacyTsCliPath,
381+
resolveTsxRuntimeImportPath,
377382
TS_CLI_ACTIONS,
378383
normalizeLegacyCliEnv,
379384
printPackageVersion,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@swarmclawai/swarmclaw",
3-
"version": "1.9.29",
3+
"version": "1.9.30",
44
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
55
"main": "electron-dist/main.js",
66
"license": "MIT",

src/cli/binary.test.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const fs = require('node:fs')
77
const os = require('node:os')
88
const path = require('node:path')
99
const { spawnSync } = require('node:child_process')
10-
const { buildLegacyTsCliArgs, resolveLegacyTsCliPath } = require('../../bin/swarmclaw.js')
10+
const { buildLegacyTsCliArgs, resolveLegacyTsCliPath, resolveTsxRuntimeImportPath } = require('../../bin/swarmclaw.js')
1111

1212
const CLI_BIN = path.join(__dirname, '..', '..', 'bin', 'swarmclaw.js')
1313
const PACKAGE_JSON = require('../../package.json')
@@ -207,21 +207,30 @@ test('package ships dagre type declarations required by installed builds', () =>
207207

208208
test('legacy TS launcher falls back to tsx import when strip-types is unavailable', () => {
209209
const cliPath = path.join(APP_ROOT, 'src', 'cli', 'index.ts')
210+
const tsxImportPath = resolveTsxRuntimeImportPath()
210211
const args = buildLegacyTsCliArgs(cliPath, ['runs', 'list'], {
211212
supportsStripTypes: false,
212213
hasTsxRuntime: true,
213214
})
214215

215-
assert.deepEqual(args, ['--no-warnings', '--import', 'tsx', cliPath, 'runs', 'list'])
216+
assert.deepEqual(args, ['--no-warnings', '--import', tsxImportPath, cliPath, 'runs', 'list'])
216217
})
217218

218219
test('legacy TS launcher uses tsx instead of strip-types inside node_modules', () => {
219220
const cliPath = path.join(os.tmpdir(), 'node_modules', '@swarmclawai', 'swarmclaw', 'src', 'cli', 'index.ts')
221+
const tsxImportPath = resolveTsxRuntimeImportPath()
220222
const args = buildLegacyTsCliArgs(cliPath, ['agents', 'list'], {
221223
supportsStripTypes: true,
222224
hasTsxRuntime: true,
223225
})
224226

225-
assert.deepEqual(args, ['--no-warnings', '--import', 'tsx', cliPath, 'agents', 'list'])
227+
assert.deepEqual(args, ['--no-warnings', '--import', tsxImportPath, cliPath, 'agents', 'list'])
226228
assert.equal(resolveLegacyTsCliPath(), path.join(APP_ROOT, 'src', 'cli', 'index.ts'))
227229
})
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+
})

src/lib/server/connectors/email.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ describe('email TLS configuration', () => {
8181
assert.equal(parseTlsRejectUnauthorized(false), false)
8282
assert.equal(parseTlsRejectUnauthorized('false'), false)
8383
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)
8588
})
8689

8790
it('handles IMAP socket errors without leaving the emitter unhandled', () => {

src/lib/server/connectors/email.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'fs'
22
import path from 'path'
3+
import type { ConnectionOptions } from 'node:tls'
34
import { ImapFlow } from 'imapflow'
45
import { createTransport, type Transporter } from 'nodemailer'
56
import { simpleParser } from 'mailparser'
@@ -34,6 +35,10 @@ interface ImapErrorEmitter {
3435
on(event: 'error', listener: (err: unknown) => void): unknown
3536
}
3637

38+
type EmailTlsOptions = Pick<ConnectionOptions, 'rejectUnauthorized' | 'checkServerIdentity'> & {
39+
rejectUnauthorized: boolean
40+
}
41+
3742
export function buildAttachments(options?: OutboundSendOptions): MailAttachment[] {
3843
const source = options?.mediaPath
3944
if (!source) return []
@@ -59,8 +64,11 @@ export function parseTlsRejectUnauthorized(value: unknown): boolean {
5964
return true
6065
}
6166

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 }
6472
}
6573

6674
export function attachImapErrorHandler(imap: ImapErrorEmitter, onDisconnected: () => void): void {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

src/lib/server/memory/dream-service.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
11
import assert from 'node:assert/strict'
22
import { describe, it } from 'node:test'
3+
import { resolveDreamGenerationPreference } from './dream-generation-preference'
34
import { parseTier2DreamResponseText } from './dream-service'
45

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+
527
describe('parseTier2DreamResponseText', () => {
628
it('parses a plain structured dream response', () => {
729
const parsed = parseTier2DreamResponseText(JSON.stringify({

src/lib/server/memory/dream-service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getMemoryDb } from '@/lib/server/memory/memory-db'
66
import { saveDreamCycle } from '@/lib/server/memory/dream-cycles'
77
import { errorMessage } from '@/lib/shared-utils'
88
import { log } from '@/lib/server/logger'
9+
import { resolveDreamGenerationPreference } from '@/lib/server/memory/dream-generation-preference'
910

1011
const TAG = 'dream-service'
1112

@@ -214,7 +215,9 @@ ${memoryLines.join('\n')}`
214215

215216
try {
216217
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' })
218221
const { HumanMessage } = await import('@langchain/core/messages')
219222

220223
const response = await llm.invoke([new HumanMessage(prompt)])

0 commit comments

Comments
 (0)