@swarmclawai/swarmclaw 1.2.1 → 1.2.3
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 +16 -85
- package/bin/server-cmd.js +64 -1
- package/package.json +2 -2
- package/skills/coding-agent/SKILL.md +111 -0
- package/skills/github/SKILL.md +140 -0
- package/skills/nano-banana-pro/SKILL.md +62 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
- package/skills/nano-pdf/SKILL.md +53 -0
- package/skills/openai-image-gen/SKILL.md +78 -0
- package/skills/openai-image-gen/scripts/gen.py +328 -0
- package/skills/resourceful-problem-solving/SKILL.md +49 -0
- package/skills/skill-creator/SKILL.md +147 -0
- package/skills/skill-creator/scripts/init_skill.py +378 -0
- package/skills/skill-creator/scripts/quick_validate.py +159 -0
- package/skills/summarize/SKILL.md +77 -0
- package/src/app/api/auth/route.ts +20 -5
- package/src/app/api/chats/[id]/devserver/route.ts +13 -19
- package/src/app/api/chats/[id]/messages/route.ts +13 -15
- package/src/app/api/chats/[id]/route.ts +9 -10
- package/src/app/api/chats/[id]/stop/route.ts +5 -7
- package/src/app/api/chats/messages-route.test.ts +8 -6
- package/src/app/api/chats/route.ts +9 -10
- package/src/app/api/ip/route.ts +2 -2
- package/src/app/api/preview-server/route.ts +1 -1
- package/src/app/api/projects/[id]/route.ts +7 -46
- package/src/cli/server-cmd.test.js +74 -0
- package/src/components/chat/chat-area.tsx +45 -23
- package/src/components/chat/message-bubble.test.ts +35 -0
- package/src/components/chat/message-bubble.tsx +19 -9
- package/src/components/chat/message-list.tsx +37 -3
- package/src/components/input/chat-input.tsx +34 -14
- package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
- package/src/instrumentation.ts +1 -1
- package/src/lib/chat/assistant-render-id.ts +3 -0
- package/src/lib/chat/chat-streaming-state.test.ts +42 -3
- package/src/lib/chat/chat-streaming-state.ts +20 -8
- package/src/lib/chat/queued-message-queue.test.ts +23 -1
- package/src/lib/chat/queued-message-queue.ts +11 -2
- package/src/lib/providers/cli-utils.test.ts +124 -0
- package/src/lib/server/activity/activity-log.ts +21 -0
- package/src/lib/server/agents/agent-availability.test.ts +10 -5
- package/src/lib/server/agents/agent-cascade.ts +79 -59
- package/src/lib/server/agents/agent-registry.ts +3 -1
- package/src/lib/server/agents/agent-repository.ts +90 -0
- package/src/lib/server/agents/delegation-job-repository.ts +53 -0
- package/src/lib/server/agents/delegation-jobs.ts +11 -4
- package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
- package/src/lib/server/agents/guardian.ts +2 -2
- package/src/lib/server/agents/main-agent-loop.ts +10 -3
- package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
- package/src/lib/server/agents/subagent-runtime.ts +9 -6
- package/src/lib/server/agents/subagent-swarm.ts +3 -2
- package/src/lib/server/agents/task-session.ts +3 -4
- package/src/lib/server/approvals/approval-repository.ts +30 -0
- package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
- package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
- package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
- package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
- package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
- package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
- package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
- package/src/lib/server/connectors/connector-repository.ts +58 -0
- package/src/lib/server/connectors/runtime-state.test.ts +117 -0
- package/src/lib/server/credentials/credential-repository.ts +7 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
- package/src/lib/server/memory/memory-abstract.test.ts +59 -0
- package/src/lib/server/missions/mission-repository.ts +74 -0
- package/src/lib/server/missions/mission-service/actions.ts +6 -0
- package/src/lib/server/missions/mission-service/bindings.ts +9 -0
- package/src/lib/server/missions/mission-service/context.ts +4 -0
- package/src/lib/server/missions/mission-service/core.ts +2269 -0
- package/src/lib/server/missions/mission-service/queries.ts +12 -0
- package/src/lib/server/missions/mission-service/recovery.ts +5 -0
- package/src/lib/server/missions/mission-service/ticks.ts +9 -0
- package/src/lib/server/missions/mission-service.test.ts +9 -2
- package/src/lib/server/missions/mission-service.ts +6 -2266
- package/src/lib/server/openclaw/deploy.test.ts +42 -3
- package/src/lib/server/openclaw/deploy.ts +26 -12
- package/src/lib/server/persistence/repository-utils.ts +154 -0
- package/src/lib/server/persistence/storage-context.ts +51 -0
- package/src/lib/server/persistence/transaction.ts +1 -0
- package/src/lib/server/projects/project-repository.ts +36 -0
- package/src/lib/server/projects/project-service.ts +79 -0
- package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
- package/src/lib/server/runtime/alert-dispatch.ts +1 -1
- package/src/lib/server/runtime/daemon-policy.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
- package/src/lib/server/runtime/daemon-state/health.ts +6 -0
- package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
- package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
- package/src/lib/server/runtime/daemon-state.test.ts +48 -0
- package/src/lib/server/runtime/daemon-state.ts +3 -1470
- package/src/lib/server/runtime/estop-repository.ts +4 -0
- package/src/lib/server/runtime/estop.ts +3 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
- package/src/lib/server/runtime/heartbeat-service.ts +55 -34
- package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
- package/src/lib/server/runtime/idle-window.ts +2 -2
- package/src/lib/server/runtime/network.ts +11 -0
- package/src/lib/server/runtime/orchestrator-events.ts +2 -2
- package/src/lib/server/runtime/queue/claims.ts +4 -0
- package/src/lib/server/runtime/queue/core.ts +2079 -0
- package/src/lib/server/runtime/queue/execution.ts +7 -0
- package/src/lib/server/runtime/queue/followups.ts +4 -0
- package/src/lib/server/runtime/queue/queries.ts +12 -0
- package/src/lib/server/runtime/queue/recovery.ts +7 -0
- package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
- package/src/lib/server/runtime/queue-repository.ts +17 -0
- package/src/lib/server/runtime/queue.ts +5 -2061
- package/src/lib/server/runtime/run-ledger.ts +6 -5
- package/src/lib/server/runtime/run-repository.ts +73 -0
- package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
- package/src/lib/server/runtime/runtime-settings.ts +1 -1
- package/src/lib/server/runtime/runtime-state.ts +99 -0
- package/src/lib/server/runtime/scheduler.ts +4 -2
- package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
- package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
- package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
- package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
- package/src/lib/server/runtime/session-run-manager.ts +72 -1377
- package/src/lib/server/runtime/watch-job-repository.ts +35 -0
- package/src/lib/server/runtime/watch-jobs.ts +3 -1
- package/src/lib/server/schedules/schedule-repository.ts +42 -0
- package/src/lib/server/sessions/session-repository.ts +85 -0
- package/src/lib/server/settings/settings-repository.ts +25 -0
- package/src/lib/server/skills/skill-discovery.test.ts +2 -2
- package/src/lib/server/skills/skill-discovery.ts +2 -2
- package/src/lib/server/skills/skill-repository.ts +14 -0
- package/src/lib/server/storage.ts +13 -24
- package/src/lib/server/tasks/task-repository.ts +54 -0
- package/src/lib/server/usage/usage-repository.ts +30 -0
- package/src/lib/server/webhooks/webhook-repository.ts +10 -0
- package/src/lib/strip-internal-metadata.test.ts +42 -41
- package/src/stores/use-chat-store.test.ts +54 -0
- package/src/stores/use-chat-store.ts +21 -5
- /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
getOpenClawLocalDeployStatus,
|
|
7
7
|
getOpenClawRemoteDeployCollectionStatus,
|
|
8
8
|
getOpenClawRemoteDeployStatus,
|
|
9
|
+
sanitizeLocalPort,
|
|
10
|
+
sanitizeSshConfig,
|
|
9
11
|
} from './deploy'
|
|
10
12
|
|
|
11
13
|
const GLOBAL_KEY = '__swarmclaw_openclaw_deploy__' as const
|
|
@@ -42,13 +44,17 @@ test('docker smart deploy bundle uses official image and provider-specific metad
|
|
|
42
44
|
|
|
43
45
|
const envFile = bundle.files.find((file) => file.name === '.env')
|
|
44
46
|
assert.ok(envFile)
|
|
45
|
-
assert.match(envFile.content, /OPENCLAW_IMAGE=openclaw:latest/)
|
|
47
|
+
assert.match(envFile.content, /OPENCLAW_IMAGE=ghcr\.io\/openclaw\/openclaw:latest/)
|
|
46
48
|
assert.match(envFile.content, /OPENCLAW_GATEWAY_TOKEN=test-token/)
|
|
47
49
|
|
|
50
|
+
const dockerCompose = bundle.files.find((file) => file.name === 'docker-compose.yml')
|
|
51
|
+
assert.ok(dockerCompose)
|
|
52
|
+
assert.match(dockerCompose.content, /image: \$\{OPENCLAW_IMAGE:-ghcr\.io\/openclaw\/openclaw:latest\}/)
|
|
53
|
+
|
|
48
54
|
const cloudInit = bundle.files.find((file) => file.name === 'cloud-init.yaml')
|
|
49
55
|
assert.ok(cloudInit)
|
|
50
56
|
assert.match(cloudInit.content, /docker\.io/)
|
|
51
|
-
assert.match(cloudInit.content, /docker pull "\$\{OPENCLAW_IMAGE:-openclaw:latest\}"/)
|
|
57
|
+
assert.match(cloudInit.content, /docker pull "\$\{OPENCLAW_IMAGE:-ghcr\.io\/openclaw\/openclaw:latest\}"/)
|
|
52
58
|
assert.match(cloudInit.content, /\/opt\/openclaw\/docker-compose\.yml/)
|
|
53
59
|
|
|
54
60
|
const caddyfile = bundle.files.find((file) => file.name === 'Caddyfile')
|
|
@@ -74,6 +80,39 @@ test('render bundle stays aligned with the official repo flow', () => {
|
|
|
74
80
|
assert.match(bundle.runbook[0] || '', /official OpenClaw GitHub repo/i)
|
|
75
81
|
})
|
|
76
82
|
|
|
83
|
+
test('remote bundle preserves low HTTP ports below 1024', () => {
|
|
84
|
+
const bundle = buildOpenClawDeployBundle({
|
|
85
|
+
template: 'docker',
|
|
86
|
+
target: 'gateway.example.com',
|
|
87
|
+
scheme: 'http',
|
|
88
|
+
port: 81,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
assert.equal(bundle.endpoint, 'http://gateway.example.com:81/v1')
|
|
92
|
+
assert.equal(bundle.wsUrl, 'ws://gateway.example.com:81')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('ssh config preserves port 22', () => {
|
|
96
|
+
const config = sanitizeSshConfig({
|
|
97
|
+
host: 'gateway.example.com',
|
|
98
|
+
port: 22,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(config, {
|
|
102
|
+
host: 'gateway.example.com',
|
|
103
|
+
user: 'root',
|
|
104
|
+
port: 22,
|
|
105
|
+
keyPath: null,
|
|
106
|
+
targetDir: '/opt/openclaw',
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('local managed deploy ports stay clamped to unprivileged values', () => {
|
|
111
|
+
assert.equal(sanitizeLocalPort(22), 1024)
|
|
112
|
+
assert.equal(sanitizeLocalPort('443'), 1024)
|
|
113
|
+
assert.equal(sanitizeLocalPort(18789), 18789)
|
|
114
|
+
})
|
|
115
|
+
|
|
77
116
|
test('local deploy status exposes a sensible default endpoint before startup', () => {
|
|
78
117
|
const status = getOpenClawLocalDeployStatus()
|
|
79
118
|
const collection = getOpenClawLocalDeployCollectionStatus()
|
|
@@ -159,7 +198,7 @@ test('remote deploy collection preserves multiple remotes and targeted lookup',
|
|
|
159
198
|
createdAt: 30,
|
|
160
199
|
updatedAt: 40,
|
|
161
200
|
lastError: null,
|
|
162
|
-
lastSummary: 'Pulling openclaw:latest and recreating the OpenClaw stack on beta.example.com.',
|
|
201
|
+
lastSummary: 'Pulling ghcr.io/openclaw/openclaw:latest and recreating the OpenClaw stack on beta.example.com.',
|
|
163
202
|
lastCommandPreview: 'ssh root@beta.example.com docker compose up -d',
|
|
164
203
|
lastBackupPath: null,
|
|
165
204
|
},
|
|
@@ -192,6 +192,7 @@ interface ExposureMeta {
|
|
|
192
192
|
|
|
193
193
|
const DEFAULT_LOCAL_PORT = 18789
|
|
194
194
|
const DEFAULT_REMOTE_PORT = 18789
|
|
195
|
+
const DEFAULT_OPENCLAW_IMAGE = 'ghcr.io/openclaw/openclaw:latest'
|
|
195
196
|
const OC_DEPLOY_KEY = '__swarmclaw_openclaw_deploy__'
|
|
196
197
|
|
|
197
198
|
const REMOTE_PROVIDER_META: Record<OpenClawRemoteDeployProvider, RemoteProviderMeta> = {
|
|
@@ -604,14 +605,27 @@ function buildLocalInstallCommand(port: number, token?: string | null, localId =
|
|
|
604
605
|
return `${parts.join(' ')} && npx openclaw gateway start`
|
|
605
606
|
}
|
|
606
607
|
|
|
607
|
-
function
|
|
608
|
+
function parsePortNumber(value: unknown): number | null {
|
|
608
609
|
const parsed = typeof value === 'number'
|
|
609
610
|
? value
|
|
610
611
|
: typeof value === 'string'
|
|
611
612
|
? Number.parseInt(value, 10)
|
|
612
613
|
: Number.NaN
|
|
613
|
-
|
|
614
|
-
|
|
614
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function sanitizePortInRange(value: unknown, fallback: number, minimum: number): number {
|
|
618
|
+
const parsed = parsePortNumber(value)
|
|
619
|
+
if (parsed === null) return fallback
|
|
620
|
+
return Math.max(minimum, Math.min(65535, Math.trunc(parsed)))
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function sanitizeLocalPort(value: unknown, fallback = DEFAULT_LOCAL_PORT): number {
|
|
624
|
+
return sanitizePortInRange(value, fallback, 1024)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function sanitizeRemotePort(value: unknown, fallback = DEFAULT_REMOTE_PORT): number {
|
|
628
|
+
return sanitizePortInRange(value, fallback, 1)
|
|
615
629
|
}
|
|
616
630
|
|
|
617
631
|
function normalizeToken(value: unknown): string | null {
|
|
@@ -666,10 +680,10 @@ function normalizeExposurePreset(value: unknown, fallback?: OpenClawUseCaseTempl
|
|
|
666
680
|
return useCase?.defaultExposure || 'private-lan'
|
|
667
681
|
}
|
|
668
682
|
|
|
669
|
-
function sanitizeSshConfig(input?: Partial<OpenClawSshConfig> | null): OpenClawSshConfig | null {
|
|
683
|
+
export function sanitizeSshConfig(input?: Partial<OpenClawSshConfig> | null): OpenClawSshConfig | null {
|
|
670
684
|
const host = typeof input?.host === 'string' && input.host.trim() ? input.host.trim() : ''
|
|
671
685
|
if (!host) return null
|
|
672
|
-
const port =
|
|
686
|
+
const port = sanitizePortInRange(input?.port, 22, 1)
|
|
673
687
|
return {
|
|
674
688
|
host,
|
|
675
689
|
user: typeof input?.user === 'string' && input.user.trim() ? input.user.trim() : 'root',
|
|
@@ -1034,7 +1048,7 @@ export async function startOpenClawLocalDeploy(input?: {
|
|
|
1034
1048
|
makePrimary?: boolean
|
|
1035
1049
|
}): Promise<{ local: OpenClawLocalDeployStatus; locals: OpenClawLocalDeployStatus[]; token: string }> {
|
|
1036
1050
|
const state = getRuntimeState()
|
|
1037
|
-
const port =
|
|
1051
|
+
const port = sanitizeLocalPort(input?.port, DEFAULT_LOCAL_PORT)
|
|
1038
1052
|
const requestedLocalId = typeof input?.localId === 'string' && input.localId.trim()
|
|
1039
1053
|
? input.localId.trim()
|
|
1040
1054
|
: null
|
|
@@ -1238,7 +1252,7 @@ function resolveHostBindAddress(useCase: OpenClawUseCaseTemplate, exposure: Open
|
|
|
1238
1252
|
function buildDockerComposeFile(options: DockerBundleOptions): string {
|
|
1239
1253
|
return `services:
|
|
1240
1254
|
openclaw-gateway:
|
|
1241
|
-
image: \${OPENCLAW_IMAGE
|
|
1255
|
+
image: \${OPENCLAW_IMAGE:-${DEFAULT_OPENCLAW_IMAGE}}
|
|
1242
1256
|
environment:
|
|
1243
1257
|
HOME: /home/node
|
|
1244
1258
|
TERM: xterm-256color
|
|
@@ -1284,7 +1298,7 @@ function buildDockerComposeFile(options: DockerBundleOptions): string {
|
|
|
1284
1298
|
}
|
|
1285
1299
|
|
|
1286
1300
|
function buildDockerEnvFile(options: DockerBundleOptions): string {
|
|
1287
|
-
return `OPENCLAW_IMAGE
|
|
1301
|
+
return `OPENCLAW_IMAGE=${DEFAULT_OPENCLAW_IMAGE}
|
|
1288
1302
|
OPENCLAW_GATEWAY_TOKEN=${options.token}
|
|
1289
1303
|
OPENCLAW_GATEWAY_BIND=lan
|
|
1290
1304
|
OPENCLAW_HOST_BIND=${resolveHostBindAddress(options.useCase, options.exposure)}
|
|
@@ -1314,7 +1328,7 @@ if ! command -v docker >/dev/null 2>&1; then
|
|
|
1314
1328
|
exit 1
|
|
1315
1329
|
fi
|
|
1316
1330
|
|
|
1317
|
-
docker pull "\${OPENCLAW_IMAGE
|
|
1331
|
+
docker pull "\${OPENCLAW_IMAGE:-${DEFAULT_OPENCLAW_IMAGE}}"
|
|
1318
1332
|
docker compose up -d
|
|
1319
1333
|
if [ -f docker-compose.proxy.yml ]; then
|
|
1320
1334
|
docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d
|
|
@@ -1365,7 +1379,7 @@ ${extraFiles ? `${extraFiles}
|
|
|
1365
1379
|
` : ''}runcmd:
|
|
1366
1380
|
- mkdir -p /opt/openclaw/.openclaw /opt/openclaw/workspace /opt/openclaw/backups
|
|
1367
1381
|
- systemctl enable --now docker
|
|
1368
|
-
- bash -lc 'cd /opt/openclaw && docker pull "\${OPENCLAW_IMAGE
|
|
1382
|
+
- bash -lc 'cd /opt/openclaw && docker pull "\${OPENCLAW_IMAGE:-${DEFAULT_OPENCLAW_IMAGE}}"'
|
|
1369
1383
|
- bash -lc 'cd /opt/openclaw && docker compose up -d'
|
|
1370
1384
|
- bash -lc 'cd /opt/openclaw && if [ -f docker-compose.proxy.yml ]; then docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d; fi'
|
|
1371
1385
|
final_message: "OpenClaw gateway bootstrap complete. Run: sudo docker compose -f /opt/openclaw/docker-compose.yml ps"
|
|
@@ -1597,7 +1611,7 @@ export function buildOpenClawDeployBundle(input?: {
|
|
|
1597
1611
|
const template = input?.template || 'docker'
|
|
1598
1612
|
const token = normalizeToken(input?.token) || generateOpenClawGatewayToken()
|
|
1599
1613
|
const scheme = input?.scheme === 'http' ? 'http' : 'https'
|
|
1600
|
-
const port =
|
|
1614
|
+
const port = sanitizeRemotePort(input?.port, DEFAULT_REMOTE_PORT)
|
|
1601
1615
|
const rawTarget = typeof input?.target === 'string' ? input.target.trim() : ''
|
|
1602
1616
|
const endpoint = normalizeOpenClawEndpoint(ensureSchemeAndPort(rawTarget || 'openclaw.example.com', scheme, port))
|
|
1603
1617
|
const wsUrl = deriveOpenClawWsUrl(endpoint)
|
|
@@ -1802,7 +1816,7 @@ export async function runOpenClawRemoteLifecycleAction(input?: {
|
|
|
1802
1816
|
const sshConfig = sanitizeSshConfig(input?.ssh)
|
|
1803
1817
|
if (!sshConfig) throw new Error('SSH host is required for remote lifecycle actions.')
|
|
1804
1818
|
const remoteDir = sshConfig.targetDir || '/opt/openclaw'
|
|
1805
|
-
const image = normalizeText(input?.image) ||
|
|
1819
|
+
const image = normalizeText(input?.image) || DEFAULT_OPENCLAW_IMAGE
|
|
1806
1820
|
const action = input?.action || 'restart'
|
|
1807
1821
|
let remoteCommand = ''
|
|
1808
1822
|
let summary = ''
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { perf } from '@/lib/server/runtime/perf'
|
|
2
|
+
|
|
3
|
+
export interface RecordRepository<
|
|
4
|
+
T,
|
|
5
|
+
ListOptions = void,
|
|
6
|
+
UpsertValue = T | Record<string, unknown>,
|
|
7
|
+
> {
|
|
8
|
+
get(id: string, options?: ListOptions): T | null
|
|
9
|
+
getMany(ids: string[], options?: ListOptions): Record<string, T>
|
|
10
|
+
list(options?: ListOptions): Record<string, T>
|
|
11
|
+
upsert(id: string, value: UpsertValue): void
|
|
12
|
+
upsertMany(entries: Array<[string, UpsertValue]>): void
|
|
13
|
+
patch(id: string, updater: (current: T | null) => T | null, options?: ListOptions): T | null
|
|
14
|
+
replace(data: Record<string, UpsertValue>): void
|
|
15
|
+
delete(id: string): void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RecordRepositoryOps<
|
|
19
|
+
T,
|
|
20
|
+
ListOptions = void,
|
|
21
|
+
UpsertValue = T | Record<string, unknown>,
|
|
22
|
+
> {
|
|
23
|
+
get(id: string, options?: ListOptions): T | null
|
|
24
|
+
list(options?: ListOptions): Record<string, T>
|
|
25
|
+
upsert(id: string, value: UpsertValue): void
|
|
26
|
+
upsertMany?: (entries: Array<[string, UpsertValue]>) => void
|
|
27
|
+
patch?: (id: string, updater: (current: T | null) => T | null) => T | null
|
|
28
|
+
replace?: (data: Record<string, UpsertValue>) => void
|
|
29
|
+
delete?: (id: string) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SingletonRepository<
|
|
33
|
+
T,
|
|
34
|
+
SaveValue = T | Record<string, unknown>,
|
|
35
|
+
> {
|
|
36
|
+
get(): T
|
|
37
|
+
save(value: SaveValue): void
|
|
38
|
+
patch(updater: (current: T) => SaveValue): T
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SingletonRepositoryOps<
|
|
42
|
+
T,
|
|
43
|
+
SaveValue = T | Record<string, unknown>,
|
|
44
|
+
> {
|
|
45
|
+
get(): T
|
|
46
|
+
save(value: SaveValue): void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function uniqueIds(ids: string[]): string[] {
|
|
50
|
+
const out: string[] = []
|
|
51
|
+
const seen = new Set<string>()
|
|
52
|
+
for (const id of ids) {
|
|
53
|
+
const normalized = typeof id === 'string' ? id.trim() : ''
|
|
54
|
+
if (!normalized || seen.has(normalized)) continue
|
|
55
|
+
seen.add(normalized)
|
|
56
|
+
out.push(normalized)
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createRecordRepository<
|
|
62
|
+
T,
|
|
63
|
+
ListOptions = void,
|
|
64
|
+
UpsertValue = T | Record<string, unknown>,
|
|
65
|
+
>(
|
|
66
|
+
name: string,
|
|
67
|
+
ops: RecordRepositoryOps<T, ListOptions, UpsertValue>,
|
|
68
|
+
): RecordRepository<T, ListOptions, UpsertValue> {
|
|
69
|
+
return {
|
|
70
|
+
get(id, options) {
|
|
71
|
+
return perf.measureSync('repository', `${name}.get`, () => ops.get(id, options), { id })
|
|
72
|
+
},
|
|
73
|
+
getMany(ids, options) {
|
|
74
|
+
return perf.measureSync('repository', `${name}.getMany`, () => {
|
|
75
|
+
const result: Record<string, T> = {}
|
|
76
|
+
for (const id of uniqueIds(ids)) {
|
|
77
|
+
const item = ops.get(id, options)
|
|
78
|
+
if (item) result[id] = item
|
|
79
|
+
}
|
|
80
|
+
return result
|
|
81
|
+
}, { count: ids.length })
|
|
82
|
+
},
|
|
83
|
+
list(options) {
|
|
84
|
+
return perf.measureSync('repository', `${name}.list`, () => ops.list(options))
|
|
85
|
+
},
|
|
86
|
+
upsert(id, value) {
|
|
87
|
+
perf.measureSync('repository', `${name}.upsert`, () => ops.upsert(id, value), { id })
|
|
88
|
+
},
|
|
89
|
+
upsertMany(entries) {
|
|
90
|
+
perf.measureSync('repository', `${name}.upsertMany`, () => {
|
|
91
|
+
if (ops.upsertMany) {
|
|
92
|
+
ops.upsertMany(entries)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
for (const [id, value] of entries) ops.upsert(id, value)
|
|
96
|
+
}, { count: entries.length })
|
|
97
|
+
},
|
|
98
|
+
patch(id, updater, options) {
|
|
99
|
+
return perf.measureSync('repository', `${name}.patch`, () => {
|
|
100
|
+
if (ops.patch) return ops.patch(id, updater)
|
|
101
|
+
const current = ops.get(id, options)
|
|
102
|
+
const next = updater(current)
|
|
103
|
+
if (next === null) {
|
|
104
|
+
if (!ops.delete) return null
|
|
105
|
+
ops.delete(id)
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
ops.upsert(id, next as UpsertValue)
|
|
109
|
+
return next
|
|
110
|
+
}, { id })
|
|
111
|
+
},
|
|
112
|
+
replace(data) {
|
|
113
|
+
perf.measureSync('repository', `${name}.replace`, () => {
|
|
114
|
+
if (ops.replace) {
|
|
115
|
+
ops.replace(data)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
const entries = Object.entries(data)
|
|
119
|
+
if (ops.upsertMany) ops.upsertMany(entries)
|
|
120
|
+
else for (const [id, value] of entries) ops.upsert(id, value)
|
|
121
|
+
}, { count: Object.keys(data).length })
|
|
122
|
+
},
|
|
123
|
+
delete(id) {
|
|
124
|
+
perf.measureSync('repository', `${name}.delete`, () => {
|
|
125
|
+
if (!ops.delete) return
|
|
126
|
+
ops.delete(id)
|
|
127
|
+
}, { id })
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createSingletonRepository<
|
|
133
|
+
T,
|
|
134
|
+
SaveValue = T | Record<string, unknown>,
|
|
135
|
+
>(
|
|
136
|
+
name: string,
|
|
137
|
+
ops: SingletonRepositoryOps<T, SaveValue>,
|
|
138
|
+
): SingletonRepository<T, SaveValue> {
|
|
139
|
+
return {
|
|
140
|
+
get() {
|
|
141
|
+
return perf.measureSync('repository', `${name}.get`, () => ops.get())
|
|
142
|
+
},
|
|
143
|
+
save(value) {
|
|
144
|
+
perf.measureSync('repository', `${name}.save`, () => ops.save(value))
|
|
145
|
+
},
|
|
146
|
+
patch(updater) {
|
|
147
|
+
return perf.measureSync('repository', `${name}.patch`, () => {
|
|
148
|
+
const next = updater(ops.get())
|
|
149
|
+
ops.save(next)
|
|
150
|
+
return ops.get()
|
|
151
|
+
})
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { withTransaction } from '@/lib/server/storage'
|
|
2
|
+
|
|
3
|
+
import { agentRepository } from '@/lib/server/agents/agent-repository'
|
|
4
|
+
import { approvalRepository } from '@/lib/server/approvals/approval-repository'
|
|
5
|
+
import { chatroomRepository } from '@/lib/server/chatrooms/chatroom-repository'
|
|
6
|
+
import { connectorRepository } from '@/lib/server/connectors/connector-repository'
|
|
7
|
+
import { missionEventRepository, missionRepository } from '@/lib/server/missions/mission-repository'
|
|
8
|
+
import { projectRepository } from '@/lib/server/projects/project-repository'
|
|
9
|
+
import { scheduleRepository } from '@/lib/server/schedules/schedule-repository'
|
|
10
|
+
import { sessionRepository } from '@/lib/server/sessions/session-repository'
|
|
11
|
+
import { settingsRepository } from '@/lib/server/settings/settings-repository'
|
|
12
|
+
import { taskRepository } from '@/lib/server/tasks/task-repository'
|
|
13
|
+
import { runEventRepository, runRepository } from '@/lib/server/runtime/run-repository'
|
|
14
|
+
|
|
15
|
+
export interface StorageTxContext {
|
|
16
|
+
agents: typeof agentRepository
|
|
17
|
+
approvals: typeof approvalRepository
|
|
18
|
+
chatrooms: typeof chatroomRepository
|
|
19
|
+
connectors: typeof connectorRepository
|
|
20
|
+
missions: typeof missionRepository
|
|
21
|
+
missionEvents: typeof missionEventRepository
|
|
22
|
+
projects: typeof projectRepository
|
|
23
|
+
runs: typeof runRepository
|
|
24
|
+
runEvents: typeof runEventRepository
|
|
25
|
+
schedules: typeof scheduleRepository
|
|
26
|
+
sessions: typeof sessionRepository
|
|
27
|
+
settings: typeof settingsRepository
|
|
28
|
+
tasks: typeof taskRepository
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createStorageTxContext(): StorageTxContext {
|
|
32
|
+
return {
|
|
33
|
+
agents: agentRepository,
|
|
34
|
+
approvals: approvalRepository,
|
|
35
|
+
chatrooms: chatroomRepository,
|
|
36
|
+
connectors: connectorRepository,
|
|
37
|
+
missions: missionRepository,
|
|
38
|
+
missionEvents: missionEventRepository,
|
|
39
|
+
projects: projectRepository,
|
|
40
|
+
runs: runRepository,
|
|
41
|
+
runEvents: runEventRepository,
|
|
42
|
+
schedules: scheduleRepository,
|
|
43
|
+
sessions: sessionRepository,
|
|
44
|
+
settings: settingsRepository,
|
|
45
|
+
tasks: taskRepository,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function withStorageTx<T>(fn: (ctx: StorageTxContext) => T): T {
|
|
50
|
+
return withTransaction(() => fn(createStorageTxContext()))
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { withTransaction } from '@/lib/server/storage'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Project } from '@/types'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
deleteProject as deleteStoredProject,
|
|
5
|
+
loadProjects as loadStoredProjects,
|
|
6
|
+
saveProjects as saveStoredProjects,
|
|
7
|
+
upsertStoredItem,
|
|
8
|
+
} from '@/lib/server/storage'
|
|
9
|
+
import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
|
|
10
|
+
|
|
11
|
+
export const projectRepository = createRecordRepository<Project>(
|
|
12
|
+
'projects',
|
|
13
|
+
{
|
|
14
|
+
get(id) {
|
|
15
|
+
return (loadStoredProjects() as Record<string, Project>)[id] || null
|
|
16
|
+
},
|
|
17
|
+
list() {
|
|
18
|
+
return loadStoredProjects() as Record<string, Project>
|
|
19
|
+
},
|
|
20
|
+
upsert(id, value) {
|
|
21
|
+
upsertStoredItem('projects', id, value)
|
|
22
|
+
},
|
|
23
|
+
replace(data) {
|
|
24
|
+
saveStoredProjects(data)
|
|
25
|
+
},
|
|
26
|
+
delete(id) {
|
|
27
|
+
deleteStoredProject(id)
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
export const loadProjects = () => projectRepository.list()
|
|
33
|
+
export const loadProject = (id: string) => projectRepository.get(id)
|
|
34
|
+
export const saveProjects = (items: Record<string, Project | Record<string, unknown>>) => projectRepository.replace(items as Record<string, Project>)
|
|
35
|
+
export const upsertProject = (id: string, value: Project | Record<string, unknown>) => projectRepository.upsert(id, value as Project)
|
|
36
|
+
export const deleteProject = (id: string) => projectRepository.delete(id)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Agent, BoardTask, Project, Schedule, Skill, StoredSecret } from '@/types'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
deleteProject as deleteStoredProject,
|
|
5
|
+
loadAgents,
|
|
6
|
+
loadProjects,
|
|
7
|
+
loadSchedules,
|
|
8
|
+
loadSecrets,
|
|
9
|
+
loadSkills,
|
|
10
|
+
loadTasks,
|
|
11
|
+
saveAgents,
|
|
12
|
+
saveProjects,
|
|
13
|
+
saveSchedules,
|
|
14
|
+
saveSecrets,
|
|
15
|
+
saveSkills,
|
|
16
|
+
saveTasks,
|
|
17
|
+
} from '@/lib/server/storage'
|
|
18
|
+
import { ensureProjectWorkspace, normalizeProjectPatchInput } from '@/lib/server/project-utils'
|
|
19
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
20
|
+
|
|
21
|
+
type ProjectLinkedRecord = {
|
|
22
|
+
projectId?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function clearProjectId<T extends ProjectLinkedRecord>(
|
|
26
|
+
projectId: string,
|
|
27
|
+
load: () => Record<string, T>,
|
|
28
|
+
save: (items: Record<string, T>) => void,
|
|
29
|
+
topic: string,
|
|
30
|
+
): void {
|
|
31
|
+
const items = load()
|
|
32
|
+
let changed = false
|
|
33
|
+
for (const item of Object.values(items)) {
|
|
34
|
+
if (item.projectId !== projectId) continue
|
|
35
|
+
item.projectId = undefined
|
|
36
|
+
changed = true
|
|
37
|
+
}
|
|
38
|
+
if (!changed) return
|
|
39
|
+
save(items)
|
|
40
|
+
notify(topic)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getProject(id: string): Project | null {
|
|
44
|
+
return loadProjects()[id] || null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function updateProject(id: string, input: Record<string, unknown>): Project | null {
|
|
48
|
+
const projects = loadProjects()
|
|
49
|
+
const existing = projects[id]
|
|
50
|
+
if (!existing) return null
|
|
51
|
+
|
|
52
|
+
const patch = normalizeProjectPatchInput(input)
|
|
53
|
+
const nextProject: Project = {
|
|
54
|
+
...existing,
|
|
55
|
+
...patch,
|
|
56
|
+
id,
|
|
57
|
+
updatedAt: Date.now(),
|
|
58
|
+
}
|
|
59
|
+
projects[id] = nextProject
|
|
60
|
+
saveProjects(projects)
|
|
61
|
+
ensureProjectWorkspace(id, nextProject.name)
|
|
62
|
+
notify('projects')
|
|
63
|
+
return nextProject
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function deleteProjectAndDetachReferences(id: string): boolean {
|
|
67
|
+
if (!getProject(id)) return false
|
|
68
|
+
|
|
69
|
+
deleteStoredProject(id)
|
|
70
|
+
notify('projects')
|
|
71
|
+
|
|
72
|
+
clearProjectId<Agent>(id, loadAgents, saveAgents, 'agents')
|
|
73
|
+
clearProjectId<BoardTask>(id, loadTasks, saveTasks, 'tasks')
|
|
74
|
+
clearProjectId<Schedule>(id, loadSchedules, saveSchedules, 'schedules')
|
|
75
|
+
clearProjectId<Skill>(id, loadSkills, saveSkills, 'skills')
|
|
76
|
+
clearProjectId<StoredSecret>(id, loadSecrets, saveSecrets, 'secrets')
|
|
77
|
+
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
@@ -3,13 +3,15 @@ import { after, before, describe, it } from 'node:test'
|
|
|
3
3
|
|
|
4
4
|
let mod: typeof import('@/lib/server/protocols/protocol-normalization')
|
|
5
5
|
|
|
6
|
+
const savedBuildMode = process.env.SWARMCLAW_BUILD_MODE
|
|
6
7
|
before(async () => {
|
|
7
8
|
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
8
9
|
mod = await import('@/lib/server/protocols/protocol-normalization')
|
|
9
10
|
})
|
|
10
11
|
|
|
11
12
|
after(() => {
|
|
12
|
-
delete process.env.SWARMCLAW_BUILD_MODE
|
|
13
|
+
if (savedBuildMode === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
14
|
+
else process.env.SWARMCLAW_BUILD_MODE = savedBuildMode
|
|
13
15
|
})
|
|
14
16
|
|
|
15
17
|
describe('protocol-normalization', () => {
|
|
@@ -26,8 +28,8 @@ describe('protocol-normalization', () => {
|
|
|
26
28
|
})
|
|
27
29
|
|
|
28
30
|
it('artifact_exists with artifactKind', () => {
|
|
29
|
-
const result = mod.normalizeCondition({ type: 'artifact_exists', artifactKind: '
|
|
30
|
-
assert.deepEqual(result, { type: 'artifact_exists', artifactKind: '
|
|
31
|
+
const result = mod.normalizeCondition({ type: 'artifact_exists', artifactKind: 'summary' })
|
|
32
|
+
assert.deepEqual(result, { type: 'artifact_exists', artifactKind: 'summary' })
|
|
31
33
|
})
|
|
32
34
|
|
|
33
35
|
it('artifact_exists without artifactKind', () => {
|
|
@@ -53,7 +55,7 @@ describe('protocol-normalization', () => {
|
|
|
53
55
|
type: 'all',
|
|
54
56
|
conditions: [
|
|
55
57
|
{ type: 'summary_exists' },
|
|
56
|
-
{ type: 'artifact_exists', artifactKind: '
|
|
58
|
+
{ type: 'artifact_exists', artifactKind: 'notes' },
|
|
57
59
|
],
|
|
58
60
|
})
|
|
59
61
|
assert.equal(result!.type, 'all')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/runtime/heartbeat-defaults'
|
|
2
|
-
import { loadSettings } from '@/lib/server/
|
|
2
|
+
import { loadSettings } from '@/lib/server/settings/settings-repository'
|
|
3
3
|
import type { Session } from '@/types'
|
|
4
4
|
|
|
5
5
|
const SYNTHETIC_HEALTH_SESSION_USERS = new Set(['workbench', 'comparison-bench'])
|