Skip to content

Commit d3620e7

Browse files
committed
Add schedule name derivation and include installer/docs/tests
1 parent 7dbd749 commit d3620e7

File tree

9 files changed

+459
-18
lines changed

9 files changed

+459
-18
lines changed

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,14 @@
33
[![CI](https://github.com/swarmclawai/swarmclaw/actions/workflows/ci.yml/badge.svg)](https://github.com/swarmclawai/swarmclaw/actions/workflows/ci.yml)
44
[![Release](https://img.shields.io/github/v/release/swarmclawai/swarmclaw?sort=semver)](https://github.com/swarmclawai/swarmclaw/releases)
55

6-
<p align="center">
7-
<img src="public/branding/swarmclaw-org-avatar.png" alt="SwarmClaw lobster logo" width="120" />
8-
</p>
9-
106
Self-hosted AI agent orchestration dashboard. Manage multiple AI providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms — all from a single mobile-friendly interface.
117

12-
SwarmClaw can connect to one or many [OpenClaw](https://github.com/openclaw/openclaw) instances and orchestrate them from a single control plane.
13-
148
Inspired by [OpenClaw](https://github.com/openclaw).
159

1610
**[Documentation](https://swarmclaw.ai/docs)** | **[Website](https://swarmclaw.ai)**
1711

12+
Org avatar files: `public/branding/swarmclaw-org-avatar.png` (upload-ready), `public/branding/swarmclaw-org-avatar.svg` (source)
13+
1814
![Dashboard](public/screenshots/dashboard.png)
1915
![Agent Builder](public/screenshots/agents.png)
2016
![Task Board](public/screenshots/tasks.png)

install.sh

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
REPO="${SWARMCLAW_REPO:-swarmclawai/swarmclaw}"
5+
INSTALL_DIR="${SWARMCLAW_DIR:-$HOME/swarmclaw}"
6+
REQUESTED_VERSION="${SWARMCLAW_VERSION:-latest}"
7+
8+
log() {
9+
printf '[install] %s\n' "$1"
10+
}
11+
12+
fail() {
13+
printf '[install] ERROR: %s\n' "$1" >&2
14+
exit 1
15+
}
16+
17+
need_cmd() {
18+
if ! command -v "$1" >/dev/null 2>&1; then
19+
fail "Required command not found: $1"
20+
fi
21+
}
22+
23+
resolve_latest_release_tag() {
24+
local api_url="https://api.github.com/repos/${REPO}/releases/latest"
25+
local tag
26+
27+
if command -v curl >/dev/null 2>&1; then
28+
tag="$(curl -fsSL "$api_url" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1 || true)"
29+
else
30+
tag=""
31+
fi
32+
33+
if [[ -z "$tag" ]]; then
34+
tag="$(git ls-remote --tags --sort='-v:refname' "https://github.com/${REPO}.git" 'v*' 2>/dev/null | awk -F'/' 'NR==1 { print $3 }')"
35+
fi
36+
37+
printf '%s' "$tag"
38+
}
39+
40+
checkout_target() {
41+
local target="$1"
42+
43+
if [[ "$target" == "main" ]]; then
44+
git checkout main >/dev/null 2>&1 || git checkout -B main origin/main
45+
git pull --ff-only origin main
46+
return
47+
fi
48+
49+
if ! [[ "$target" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.+][0-9A-Za-z.-]+)?$ ]]; then
50+
fail "Resolved version is not a valid release tag: $target"
51+
fi
52+
53+
git fetch --tags origin --quiet
54+
if ! git rev-parse "refs/tags/${target}" >/dev/null 2>&1; then
55+
fail "Release tag not found: ${target}"
56+
fi
57+
git checkout -B stable "refs/tags/${target}"
58+
}
59+
60+
main() {
61+
need_cmd git
62+
need_cmd node
63+
need_cmd npm
64+
need_cmd sed
65+
need_cmd awk
66+
67+
local target="$REQUESTED_VERSION"
68+
if [[ "$target" == "latest" ]]; then
69+
target="$(resolve_latest_release_tag)"
70+
if [[ -z "$target" ]]; then
71+
log "No stable release tag found yet. Falling back to main branch."
72+
target="main"
73+
fi
74+
fi
75+
76+
log "Installing ${REPO} (${target}) into ${INSTALL_DIR}"
77+
78+
if [[ -d "$INSTALL_DIR/.git" ]]; then
79+
log "Existing install detected; updating repository metadata."
80+
git -C "$INSTALL_DIR" remote set-url origin "https://github.com/${REPO}.git"
81+
git -C "$INSTALL_DIR" fetch --all --tags --prune --quiet
82+
else
83+
rm -rf "$INSTALL_DIR"
84+
git clone "https://github.com/${REPO}.git" "$INSTALL_DIR"
85+
fi
86+
87+
cd "$INSTALL_DIR"
88+
checkout_target "$target"
89+
90+
log "Installing dependencies"
91+
npm install
92+
93+
log "Bootstrapping local environment"
94+
npm run setup:easy -- --skip-install
95+
96+
cat <<EOF
97+
98+
SwarmClaw installed successfully.
99+
100+
Next steps:
101+
1. cd "$INSTALL_DIR"
102+
2. npm run dev
103+
3. Open http://localhost:3456
104+
105+
For updates later:
106+
- npm run update:easy
107+
EOF
108+
}
109+
110+
main "$@"
111+

scripts/easy-update.mjs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { spawnSync } from 'node:child_process'
55
const args = new Set(process.argv.slice(2))
66
const skipBuild = args.has('--skip-build')
77
const allowDirty = args.has('--allow-dirty')
8+
const forceMain = args.has('--main')
89
const cwd = process.cwd()
10+
const RELEASE_TAG_RE = /^v\d+\.\d+\.\d+([-.+][0-9A-Za-z.-]+)?$/
911

1012
function log(message) {
1113
process.stdout.write(`[update] ${message}\n`)
@@ -45,6 +47,13 @@ function runOrThrow(command, commandArgs, options = {}) {
4547
}
4648
}
4749

50+
function getLatestStableTag() {
51+
const tagList = run('git', ['tag', '--list', 'v*', '--sort=-v:refname'])
52+
if (!tagList.ok) return null
53+
const tags = tagList.out.split('\n').map((line) => line.trim()).filter(Boolean)
54+
return tags.find((tag) => RELEASE_TAG_RE.test(tag)) || null
55+
}
56+
4857
function main() {
4958
const gitCheck = run('git', ['rev-parse', '--is-inside-work-tree'])
5059
if (!gitCheck.ok) {
@@ -68,17 +77,40 @@ function main() {
6877
fail('Could not resolve current git SHA.')
6978
}
7079

71-
runOrThrow('git', ['fetch', 'origin', 'main', '--quiet'])
72-
const behind = run('git', ['rev-list', 'HEAD..origin/main', '--count'])
73-
const behindBy = Number.parseInt(behind.out || '0', 10) || 0
80+
runOrThrow('git', ['fetch', '--tags', 'origin', '--quiet'])
7481

75-
if (behindBy <= 0) {
76-
log('Already up to date. Nothing to install.')
77-
return
78-
}
82+
let updateSource = 'main'
83+
let pullOutput = ''
84+
const latestTag = forceMain ? null : getLatestStableTag()
85+
86+
if (latestTag) {
87+
const behind = run('git', ['rev-list', `HEAD..${latestTag}^{commit}`, '--count'])
88+
const behindBy = Number.parseInt(behind.out || '0', 10) || 0
89+
90+
if (behindBy <= 0) {
91+
log(`Already on latest stable release (${latestTag}) or newer.`)
92+
return
93+
}
7994

80-
log(`Found ${behindBy} new commit(s). Updating now...`)
81-
runOrThrow('git', ['pull', '--ff-only', 'origin', 'main'])
95+
updateSource = `stable release ${latestTag}`
96+
log(`Found ${behindBy} commit(s) behind ${latestTag}. Updating now...`)
97+
runOrThrow('git', ['checkout', '-B', 'stable', `${latestTag}^{commit}`])
98+
pullOutput = `Updated to ${latestTag}`
99+
} else {
100+
runOrThrow('git', ['fetch', 'origin', 'main', '--quiet'])
101+
const behind = run('git', ['rev-list', 'HEAD..origin/main', '--count'])
102+
const behindBy = Number.parseInt(behind.out || '0', 10) || 0
103+
104+
if (behindBy <= 0) {
105+
log('Already up to date. Nothing to install.')
106+
return
107+
}
108+
109+
updateSource = 'main branch'
110+
log(`Found ${behindBy} new commit(s) on origin/main. Updating now...`)
111+
runOrThrow('git', ['pull', '--ff-only', 'origin', 'main'])
112+
pullOutput = `Pulled origin/main (+${behindBy})`
113+
}
82114

83115
const changed = run('git', ['diff', '--name-only', `${beforeSha.out}..HEAD`])
84116
const changedFiles = new Set((changed.out || '').split('\n').map((s) => s.trim()).filter(Boolean))
@@ -97,6 +129,7 @@ function main() {
97129
}
98130

99131
log('Update complete.')
132+
log(`Source: ${updateSource}. ${pullOutput}`.trim())
100133
log('Restart SwarmClaw to apply the new version.')
101134
}
102135

src/app/api/schedules/[id]/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextResponse } from 'next/server'
22
import { loadSchedules, saveSchedules } from '@/lib/server/storage'
3+
import { resolveScheduleName } from '@/lib/schedule-name'
34

45
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
56
const { id } = await params
@@ -10,6 +11,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
1011
const origId = id
1112
Object.assign(schedules[id], body)
1213
schedules[id].id = origId
14+
schedules[id].name = resolveScheduleName({
15+
name: schedules[id].name,
16+
taskPrompt: schedules[id].taskPrompt,
17+
})
1318
saveSchedules(schedules)
1419
return NextResponse.json(schedules[id])
1520
}

src/app/api/schedules/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextResponse } from 'next/server'
22
import crypto from 'crypto'
33
import { loadSchedules, saveSchedules } from '@/lib/server/storage'
4+
import { resolveScheduleName } from '@/lib/schedule-name'
45

56
export async function GET() {
67
return NextResponse.json(loadSchedules())
@@ -24,7 +25,7 @@ export async function POST(req: Request) {
2425

2526
schedules[id] = {
2627
id,
27-
name: body.name || 'Unnamed Schedule',
28+
name: resolveScheduleName({ name: body.name, taskPrompt: body.taskPrompt }),
2829
agentId: body.agentId,
2930
taskPrompt: body.taskPrompt || '',
3031
scheduleType: body.scheduleType || 'cron',

src/components/schedules/schedule-sheet.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export function ScheduleSheet() {
138138

139139
const handleSave = async () => {
140140
const data = {
141-
name: name.trim() || 'Unnamed Schedule',
141+
name: name.trim(),
142142
agentId,
143143
taskPrompt,
144144
scheduleType,

src/lib/schedule-name.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const MAX_SCHEDULE_NAME_LENGTH = 80
2+
3+
function normalizeWhitespace(value: string): string {
4+
return value.replace(/\s+/g, ' ').trim()
5+
}
6+
7+
function truncate(value: string, maxLength = MAX_SCHEDULE_NAME_LENGTH): string {
8+
if (value.length <= maxLength) return value
9+
return `${value.slice(0, Math.max(0, maxLength - 1)).trimEnd()}...`
10+
}
11+
12+
function isGenericName(name: string): boolean {
13+
const normalized = normalizeWhitespace(name).toLowerCase()
14+
return normalized === '' || normalized === 'schedule' || normalized === 'new schedule' || normalized === 'unnamed schedule'
15+
}
16+
17+
function deriveFromPrompt(taskPrompt: string): string {
18+
const prompt = normalizeWhitespace(taskPrompt)
19+
if (!prompt) return 'Scheduled Task'
20+
21+
const lower = prompt.toLowerCase()
22+
if (lower.includes('wikipedia') && (lower.includes('screenshot') || lower.includes('screen shot'))) {
23+
return 'Wikipedia Screenshot'
24+
}
25+
if (lower.includes('screenshot')) {
26+
return 'Screenshot Task'
27+
}
28+
if (lower.includes('backup')) {
29+
return 'Backup Task'
30+
}
31+
if (lower.includes('health check') || lower.includes('heartbeat')) {
32+
return 'Health Check'
33+
}
34+
if (lower.includes('report')) {
35+
return 'Report Task'
36+
}
37+
38+
const firstLine = prompt.split('\n')[0] || prompt
39+
const firstClause = firstLine.split(/[.,;:!?]/)[0] || firstLine
40+
const cleaned = normalizeWhitespace(
41+
firstClause
42+
.replace(/^(please\s+)?(can you|could you|would you)\s+/i, '')
43+
.replace(/^(create|make|set up|setup|schedule|run|execute|trigger|perform|generate|send|take|capture|navigate|go|open|check|monitor|fetch|pull|build|test)\b\s*/i, '')
44+
.replace(/^to\s+/i, ''),
45+
)
46+
if (!cleaned) return 'Scheduled Task'
47+
return `${cleaned.charAt(0).toUpperCase()}${cleaned.slice(1)}`
48+
}
49+
50+
export function resolveScheduleName(input: {
51+
name?: unknown
52+
taskPrompt?: unknown
53+
}): string {
54+
const providedName = typeof input.name === 'string' ? normalizeWhitespace(input.name) : ''
55+
if (providedName && !isGenericName(providedName)) {
56+
return truncate(providedName)
57+
}
58+
59+
const taskPrompt = typeof input.taskPrompt === 'string' ? input.taskPrompt : ''
60+
return truncate(deriveFromPrompt(taskPrompt))
61+
}
62+

src/lib/server/session-tools.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
import { log } from './logger'
3737
import { queryLogs, countLogs, clearLogs, type LogCategory } from './execution-log'
3838
import { resolveSessionToolPolicy } from './tool-capability-policy'
39+
import { resolveScheduleName } from '@/lib/schedule-name'
3940

4041
const MAX_OUTPUT = 50 * 1024 // 50KB
4142
const MAX_FILE = 100 * 1024 // 100KB
@@ -1859,7 +1860,7 @@ export function buildSessionTools(cwd: string, enabledTools: string[], ctx?: Too
18591860
manage_schedules: (p) => {
18601861
const now = Date.now()
18611862
const base = {
1862-
name: p.name || 'Unnamed Schedule',
1863+
name: resolveScheduleName({ name: p.name, taskPrompt: p.taskPrompt }),
18631864
agentId: p.agentId || null,
18641865
taskPrompt: p.taskPrompt || '',
18651866
scheduleType: p.scheduleType || 'interval',

0 commit comments

Comments
 (0)