@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.
- package/bin/doctor-cmd.js +149 -0
- package/bin/doctor-cmd.test.js +50 -0
- package/bin/install-root.js +39 -0
- package/bin/install-root.test.js +60 -0
- package/bin/server-cmd.js +37 -6
- package/bin/swarmclaw.js +83 -3
- package/bin/update-cmd.js +1 -6
- package/bin/update-cmd.test.js +1 -36
- package/bin/worker-cmd.js +8 -2
- package/package.json +9 -8
- package/src/app/api/gateways/[id]/health/route.ts +2 -32
- package/src/app/api/gateways/health-route.test.ts +1 -1
- package/src/app/api/setup/check-provider/helpers.ts +28 -0
- package/src/app/api/setup/check-provider/route.test.ts +1 -1
- package/src/app/api/setup/check-provider/route.ts +5 -32
- package/src/app/api/tasks/import/github/helpers.ts +100 -0
- package/src/app/api/tasks/import/github/route.test.ts +1 -1
- package/src/app/api/tasks/import/github/route.ts +2 -92
- package/src/app/api/webhooks/[id]/helpers.ts +253 -0
- package/src/app/api/webhooks/[id]/route.ts +2 -243
- package/src/app/api/webhooks/route.test.ts +4 -2
- package/src/cli/binary.test.js +57 -0
- package/src/cli/index.js +13 -1
- package/src/lib/server/data-dir.test.ts +35 -0
- package/src/lib/server/data-dir.ts +11 -0
- package/src/lib/server/openclaw/health.ts +30 -1
- package/src/lib/server/session-tools/file-send.test.ts +18 -2
- package/src/lib/server/session-tools/file.ts +11 -7
- package/src/lib/server/skills/skill-discovery.test.ts +34 -1
- package/src/lib/server/skills/skill-discovery.ts +9 -4
|
@@ -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
|
|
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(
|
|
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
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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(
|
|
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:
|
|
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 (
|
|
106
|
-
const workspaceDir =
|
|
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/)
|