@swarmclawai/swarmclaw 1.0.0 → 1.0.2

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.
@@ -13,9 +13,16 @@ function isBuildBootstrapEnv(env: NodeJS.ProcessEnv = process.env, argv: string[
13
13
 
14
14
  export const IS_BUILD_BOOTSTRAP = isBuildBootstrapEnv()
15
15
 
16
+ function resolveSwarmclawHome(): string | null {
17
+ const configured = process.env.SWARMCLAW_HOME?.trim()
18
+ return configured ? path.resolve(configured) : null
19
+ }
20
+
16
21
  function resolveDataDir(): string {
17
22
  if (process.env.DATA_DIR) return process.env.DATA_DIR
18
23
  if (IS_BUILD_BOOTSTRAP) return path.join(os.tmpdir(), 'swarmclaw-build-data')
24
+ const appHome = resolveSwarmclawHome()
25
+ if (appHome) return path.join(appHome, 'data')
19
26
  return path.join(process.cwd(), 'data')
20
27
  }
21
28
 
@@ -41,6 +48,8 @@ function supportsChildWrites(dir: string): boolean {
41
48
  function resolveWorkspaceDir(): string {
42
49
  if (process.env.WORKSPACE_DIR) return process.env.WORKSPACE_DIR
43
50
  if (IS_BUILD_BOOTSTRAP) return path.join(DATA_DIR, 'workspace')
51
+ const appHome = resolveSwarmclawHome()
52
+ if (appHome) return path.join(appHome, 'workspace')
44
53
  const external = path.join(os.homedir(), '.swarmclaw', 'workspace')
45
54
  if (supportsChildWrites(external)) {
46
55
  return external
@@ -53,6 +62,8 @@ export const WORKSPACE_DIR = resolveWorkspaceDir()
53
62
  function resolveBrowserProfilesDir(): string {
54
63
  if (process.env.BROWSER_PROFILES_DIR) return process.env.BROWSER_PROFILES_DIR
55
64
  if (IS_BUILD_BOOTSTRAP) return path.join(DATA_DIR, 'browser-profiles')
65
+ const appHome = resolveSwarmclawHome()
66
+ if (appHome) return path.join(appHome, 'browser-profiles')
56
67
  const external = path.join(os.homedir(), '.swarmclaw', 'browser-profiles')
57
68
  if (supportsChildWrites(external)) {
58
69
  return external
@@ -1,6 +1,8 @@
1
1
  import { deriveOpenClawWsUrl, normalizeOpenClawEndpoint } from '@/lib/openclaw/openclaw-endpoint'
2
2
  import { wsConnect } from '@/lib/providers/openclaw'
3
- import { decryptKey, loadCredentials } from '../storage'
3
+ import { decryptKey, loadCredentials, loadGatewayProfiles, saveGatewayProfiles } from '../storage'
4
+ import { notify } from '../ws-hub'
5
+ import type { GatewayProfile } from '@/types'
4
6
 
5
7
  export interface OpenClawHealthInput {
6
8
  endpoint?: string | null
@@ -411,3 +413,30 @@ export async function probeOpenClawHealth(input: OpenClawHealthInput): Promise<O
411
413
  hint: http.hint,
412
414
  }
413
415
  }
416
+
417
+ export function persistGatewayHealthResult(
418
+ id: string,
419
+ result: OpenClawHealthResult,
420
+ now = Date.now(),
421
+ ): GatewayProfile | null {
422
+ const gateways = loadGatewayProfiles()
423
+ const gateway = gateways[id]
424
+ if (!gateway) return null
425
+
426
+ gateway.status = result.ok ? 'healthy' : (result.authProvided ? 'degraded' : 'offline')
427
+ gateway.lastCheckedAt = now
428
+ gateway.lastError = result.ok ? null : (result.error || result.hint || 'Gateway health check failed.')
429
+ gateway.lastModelCount = Array.isArray(result.models) ? result.models.length : 0
430
+ gateway.deployment = {
431
+ ...(gateway.deployment || {}),
432
+ lastVerifiedAt: now,
433
+ lastVerifiedOk: result.ok,
434
+ lastVerifiedMessage: result.ok
435
+ ? result.message
436
+ : (result.error || result.hint || 'Gateway health check failed.'),
437
+ }
438
+ gateway.updatedAt = now
439
+ saveGatewayProfiles(gateways)
440
+ notify('gateways')
441
+ return gateway
442
+ }
@@ -113,10 +113,26 @@ describe('normalizeSendFilePaths', () => {
113
113
  fs.rmSync(cwd, { recursive: true, force: true })
114
114
  })
115
115
 
116
- it('resolves browser profile screenshot paths back into the agent home directory', () => {
116
+ it('resolves browser profile screenshot paths into the configured browser profiles directory', () => {
117
117
  const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'send-file-browser-profile-'))
118
118
  const resolved = resolveSendFileSourcePath(cwd, '../../../.swarmclaw/browser-profiles/example/mcp-output/page.png')
119
- assert.match(resolved, new RegExp(`\\.swarmclaw[\\\\/]browser-profiles[\\\\/]example[\\\\/]mcp-output[\\\\/]page\\.png$`))
119
+ assert.match(resolved, new RegExp(`browser-profiles[\\\\/]example[\\\\/]mcp-output[\\\\/]page\\.png$`))
120
120
  fs.rmSync(cwd, { recursive: true, force: true })
121
121
  })
122
+
123
+ it('resolves browser profile screenshot paths from BROWSER_PROFILES_DIR when set', () => {
124
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'send-file-browser-profile-env-'))
125
+ const browserProfilesDir = path.join(cwd, '.swarmclaw', 'browser-profiles')
126
+ const previousDir = process.env.BROWSER_PROFILES_DIR
127
+
128
+ try {
129
+ process.env.BROWSER_PROFILES_DIR = browserProfilesDir
130
+ const resolved = resolveSendFileSourcePath(cwd, '../../../.swarmclaw/browser-profiles/example/mcp-output/page.png')
131
+ assert.equal(resolved, path.join(browserProfilesDir, 'example', 'mcp-output', 'page.png'))
132
+ } finally {
133
+ if (previousDir === undefined) delete process.env.BROWSER_PROFILES_DIR
134
+ else process.env.BROWSER_PROFILES_DIR = previousDir
135
+ fs.rmSync(cwd, { recursive: true, force: true })
136
+ }
137
+ })
122
138
  })
@@ -1,10 +1,9 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import fs from 'fs'
4
- import os from 'os'
5
4
  import path from 'path'
6
5
  import { UPLOAD_DIR } from '../storage'
7
- import { WORKSPACE_DIR } from '../data-dir'
6
+ import { BROWSER_PROFILES_DIR, WORKSPACE_DIR } from '../data-dir'
8
7
  import type { ToolBuildContext } from './context'
9
8
  import { safePath, truncate, listDirRecursive, MAX_FILE } from './context'
10
9
  import type { Plugin, PluginHooks } from '@/types'
@@ -387,19 +386,24 @@ export function findRecentSendFileFallbackPaths(cwd: string, maxAgeMs = 10 * 60
387
386
  return dedup(candidates)
388
387
  }
389
388
 
389
+ function getBrowserProfilesDir(): string {
390
+ return process.env.BROWSER_PROFILES_DIR || BROWSER_PROFILES_DIR
391
+ }
392
+
390
393
  export function resolveSendFileSourcePath(cwd: string, rawPath: string, scope?: 'workspace' | 'machine'): string {
391
394
  const trimmed = rawPath.trim()
392
395
  const uploadMatch = trimmed.match(/^(?:sandbox:)?\/api\/uploads\/(.+)$/)
393
396
  if (uploadMatch) {
394
397
  return path.join(UPLOAD_DIR, path.basename(uploadMatch[1]))
395
398
  }
396
- const browserProfileIdx = trimmed.lastIndexOf('.swarmclaw/browser-profiles/')
397
- if (browserProfileIdx !== -1) {
398
- const relative = trimmed.slice(browserProfileIdx)
399
- return path.join(os.homedir(), relative)
399
+ const browserProfileMatch = trimmed
400
+ .replaceAll('\\', '/')
401
+ .match(/(?:^|\/)\.swarmclaw\/browser-profiles\/(.+)$/)
402
+ if (browserProfileMatch) {
403
+ return path.join(getBrowserProfilesDir(), browserProfileMatch[1])
400
404
  }
401
405
  if (trimmed.startsWith('browser-profiles/')) {
402
- const candidate = path.join(os.homedir(), '.swarmclaw', trimmed)
406
+ const candidate = path.join(getBrowserProfilesDir(), trimmed.slice('browser-profiles/'.length))
403
407
  if (fs.existsSync(candidate)) return candidate
404
408
  }
405
409
  if (trimmed === '/workspace' || trimmed === 'workspace') return cwd
@@ -1,7 +1,9 @@
1
1
  import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
2
4
  import path from 'node:path'
3
5
  import test from 'node:test'
4
- import { discoverSkills } from './skill-discovery'
6
+ import { clearDiscoveredSkillsCache, discoverSkills } from './skill-discovery'
5
7
 
6
8
  test('discoverSkills includes tracked bundled skills from bundled-skills', () => {
7
9
  const skills = discoverSkills({ cwd: path.join(process.cwd(), 'src') })
@@ -14,3 +16,34 @@ test('discoverSkills includes tracked bundled skills from bundled-skills', () =>
14
16
  true,
15
17
  )
16
18
  })
19
+
20
+ test('discoverSkills reads workspace skills from SWARMCLAW_HOME when set', () => {
21
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-skills-home-'))
22
+ const skillDir = path.join(tempHome, 'skills', 'local-skill')
23
+ const previousHome = process.env.SWARMCLAW_HOME
24
+
25
+ try {
26
+ fs.mkdirSync(skillDir, { recursive: true })
27
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), `---
28
+ name: local-skill
29
+ description: A local test skill.
30
+ ---
31
+
32
+ # Local Skill
33
+ `, 'utf8')
34
+ process.env.SWARMCLAW_HOME = tempHome
35
+ clearDiscoveredSkillsCache()
36
+
37
+ const skills = discoverSkills()
38
+ const localSkill = skills.find((skill) => skill.name === 'local-skill')
39
+
40
+ assert.ok(localSkill)
41
+ assert.equal(localSkill?.source, 'workspace')
42
+ assert.equal(localSkill?.sourcePath, path.join(skillDir, 'SKILL.md'))
43
+ } finally {
44
+ clearDiscoveredSkillsCache()
45
+ if (previousHome === undefined) delete process.env.SWARMCLAW_HOME
46
+ else process.env.SWARMCLAW_HOME = previousHome
47
+ fs.rmSync(tempHome, { recursive: true, force: true })
48
+ }
49
+ })
@@ -29,7 +29,12 @@ export function clearDiscoveredSkillsCache(): void {
29
29
  }
30
30
 
31
31
  function buildCacheKey(cwd?: string): string {
32
- return `${cwd || ''}`
32
+ return `${cwd || ''}|${resolveWorkspaceSkillsDir()}`
33
+ }
34
+
35
+ function resolveWorkspaceSkillsDir(): string {
36
+ const swarmclawHome = process.env.SWARMCLAW_HOME || path.join(os.homedir(), '.swarmclaw')
37
+ return path.join(swarmclawHome, 'skills')
33
38
  }
34
39
 
35
40
  function scanLayer(
@@ -81,7 +86,7 @@ function scanLayer(
81
86
  * Discover skills from three layers:
82
87
  * 1. Bundled: `bundled-skills/` (tracked with the app)
83
88
  * Legacy fallback: `data/skills/`
84
- * 2. Workspace: `~/.swarmclaw/skills/` (user-installed)
89
+ * 2. Workspace: `<swarmclaw-home>/skills/` (user-installed)
85
90
  * 3. Project: `<cwd>/skills/` (project-local)
86
91
  *
87
92
  * Results are cached with a 5-second TTL. Later layers override
@@ -102,8 +107,8 @@ export function discoverSkills(opts?: { cwd?: string }): DiscoveredSkill[] {
102
107
  ...scanLayer(BUNDLED_SKILLS_DIR, 'bundled'),
103
108
  ]
104
109
 
105
- // Layer 2: Workspace skills (~/.swarmclaw/skills/)
106
- const workspaceDir = path.join(os.homedir(), '.swarmclaw', 'skills')
110
+ // Layer 2: Workspace skills (<swarmclaw-home>/skills/)
111
+ const workspaceDir = resolveWorkspaceSkillsDir()
107
112
  const workspace = scanLayer(workspaceDir, 'workspace')
108
113
 
109
114
  // Layer 3: Project-local skills (<cwd>/skills/)