Skip to content

Commit 2e3ea55

Browse files
drpedapaticlaude
andcommitted
feat: version display, gateway mismatch detection, doctor VM conflict check, branding cleanup
- Show binary version in TUI title bar (sciClaw v{version}) - Gateway writes status file on startup for version tracking - TUI warns when gateway version mismatches binary (suppressed for dev builds) - Doctor check detects host+VM Discord/Telegram gateway conflicts (fixes #72) - Rebrand user-facing picoclaw references to sciclaw (error messages, help text, system prompt, workspace templates) - Add `make app` target for quick dev build+run - Add `sciclaw` to .gitignore, move announcement to docs/ - Update docs with host+VM conflict warning Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fb89820 commit 2e3ea55

File tree

15 files changed

+293
-83
lines changed

15 files changed

+293
-83
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ build/
88
*.dylib
99
*.test
1010
*.out
11+
/sciclaw
1112
/picoclaw
1213
/picoclaw-test
1314

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ deps:
178178
@$(GO) mod vendor
179179
@echo "Dependencies updated and vendored"
180180

181+
## app: Build and launch the TUI dashboard
182+
app: build
183+
@$(BUILD_DIR)/$(PRIMARY_BINARY_NAME) app
184+
181185
## run: Build and run sciclaw (picoclaw-compatible)
182186
run: build
183187
@$(BUILD_DIR)/$(PRIMARY_BINARY_NAME) $(ARGS)

cmd/picoclaw/doctor_cmd.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"strings"
1616
"time"
1717

18+
"github.com/sipeed/picoclaw/cmd/picoclaw/tui"
1819
"github.com/sipeed/picoclaw/pkg/auth"
1920
"github.com/sipeed/picoclaw/pkg/config"
2021
svcmgr "github.com/sipeed/picoclaw/pkg/service"
@@ -310,6 +311,9 @@ func runDoctor(opts doctorOptions) doctorReport {
310311
add(c)
311312
}
312313

314+
// Host+VM Discord conflict detection (issue #72).
315+
add(checkHostVMChannelConflict(cfg))
316+
313317
// Optional: Homebrew outdated status (best-effort).
314318
add(checkHomebrewOutdated())
315319

@@ -521,6 +525,61 @@ func checkWorkspacePythonVenv(workspace string, opts doctorOptions) doctorCheck
521525
}
522526
}
523527

528+
// checkHostVMChannelConflict detects when both host and VM gateways are
529+
// running with the same channel enabled, which causes duplicate replies.
530+
func checkHostVMChannelConflict(hostCfg *config.Config) doctorCheck {
531+
name := "gateway.host_vm_conflict"
532+
533+
if hostCfg == nil {
534+
return doctorCheck{Name: name, Status: doctorSkip, Message: "no host config"}
535+
}
536+
537+
// Quick check: is a VM even present and running?
538+
vmState := tui.VMState()
539+
if vmState != "Running" {
540+
return doctorCheck{Name: name, Status: doctorSkip, Message: "no VM running"}
541+
}
542+
543+
// Is the VM gateway service active?
544+
if !tui.VMServiceActive() {
545+
return doctorCheck{Name: name, Status: doctorOK, Message: "VM running but gateway not active"}
546+
}
547+
548+
// Read VM config to check which channels are enabled.
549+
vmCfgRaw, err := tui.VMCatFile("/home/ubuntu/.picoclaw/config.json")
550+
if err != nil {
551+
return doctorCheck{Name: name, Status: doctorSkip, Message: "cannot read VM config"}
552+
}
553+
var vmCfg struct {
554+
Channels struct {
555+
Discord struct{ Enabled bool `json:"enabled"` } `json:"discord"`
556+
Telegram struct{ Enabled bool `json:"enabled"` } `json:"telegram"`
557+
} `json:"channels"`
558+
}
559+
if json.Unmarshal([]byte(vmCfgRaw), &vmCfg) != nil {
560+
return doctorCheck{Name: name, Status: doctorSkip, Message: "cannot parse VM config"}
561+
}
562+
563+
// Check for overlapping channels.
564+
var conflicts []string
565+
if hostCfg.Channels.Discord.Enabled && vmCfg.Channels.Discord.Enabled {
566+
conflicts = append(conflicts, "Discord")
567+
}
568+
if hostCfg.Channels.Telegram.Enabled && vmCfg.Channels.Telegram.Enabled {
569+
conflicts = append(conflicts, "Telegram")
570+
}
571+
572+
if len(conflicts) == 0 {
573+
return doctorCheck{Name: name, Status: doctorOK, Message: "no channel overlap between host and VM"}
574+
}
575+
576+
return doctorCheck{
577+
Name: name,
578+
Status: doctorErr,
579+
Message: fmt.Sprintf("%s enabled on both host and VM gateways — duplicate replies will occur; disable on one side or use separate bot tokens with non-overlapping routed channels", strings.Join(conflicts, ", ")),
580+
}
581+
}
582+
524583
func checkGatewayLog(telegramEnabled bool) doctorCheck {
525584
home, err := os.UserHomeDir()
526585
if err != nil {

cmd/picoclaw/main.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"time"
2222

2323
"github.com/chzyer/readline"
24+
"github.com/sipeed/picoclaw/cmd/picoclaw/tui"
2425
"github.com/sipeed/picoclaw/pkg/agent"
2526
"github.com/sipeed/picoclaw/pkg/auth"
2627
"github.com/sipeed/picoclaw/pkg/bus"
@@ -51,6 +52,7 @@ func init() {
5152
// Strip leading "v" set by ldflags so format strings can add their own.
5253
version = strings.TrimPrefix(version, "v")
5354
agent.Version = version
55+
tui.Version = version
5456
}
5557

5658
const logo = "🔬"
@@ -260,7 +262,7 @@ func printHelp() {
260262
fmt.Println(" doctor Check deployment health and dependencies")
261263
fmt.Println(" cron Manage scheduled tasks")
262264
fmt.Println(" routing Manage channel->workspace routing and ACLs")
263-
fmt.Println(" migrate Migrate from OpenClaw to sciClaw (PicoClaw-compatible)")
265+
fmt.Println(" migrate Migrate from OpenClaw to sciClaw")
264266
fmt.Println(" skills Manage skills (install, list, remove)")
265267
fmt.Println(" backup Backup key sciClaw config/workspace files")
266268
fmt.Println(" version Show version information")
@@ -958,7 +960,7 @@ func migrateCmd() {
958960

959961
func migrateHelp() {
960962
commandName := invokedCLIName()
961-
fmt.Println("\nMigrate from OpenClaw to sciClaw (PicoClaw-compatible)")
963+
fmt.Println("\nMigrate from OpenClaw to sciClaw")
962964
fmt.Println()
963965
fmt.Printf("Usage: %s migrate [options]\n", commandName)
964966
fmt.Println()
@@ -1222,6 +1224,17 @@ func gatewayCmd() {
12221224
"skills_available": skillsInfo["available"],
12231225
})
12241226

1227+
// Write gateway status file for TUI version mismatch detection.
1228+
gwHome, _ := os.UserHomeDir()
1229+
gatewayStatusPath := filepath.Join(gwHome, ".picoclaw", "gateway.status.json")
1230+
if statusJSON, err := json.Marshal(map[string]interface{}{
1231+
"version": version,
1232+
"pid": os.Getpid(),
1233+
"started_at": time.Now().UTC().Format(time.RFC3339),
1234+
}); err == nil {
1235+
_ = os.WriteFile(gatewayStatusPath, statusJSON, 0644)
1236+
}
1237+
12251238
// Setup cron tool and service
12261239
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace)
12271240

@@ -1342,6 +1355,7 @@ func gatewayCmd() {
13421355
}
13431356
agentLoop.Stop()
13441357
channelManager.StopAll(ctx)
1358+
_ = os.Remove(gatewayStatusPath)
13451359
fmt.Println("✓ Gateway stopped")
13461360
}
13471361

@@ -2278,7 +2292,7 @@ func skillsHelp() {
22782292
fmt.Println()
22792293
fmt.Println("Examples:")
22802294
fmt.Printf(" %s skills list\n", commandName)
2281-
fmt.Printf(" %s skills install sipeed/picoclaw-skills/weather\n", commandName)
2295+
fmt.Printf(" %s skills install drpedapati/sciclaw-skills/weather\n", commandName)
22822296
fmt.Printf(" %s skills install-builtin\n", commandName)
22832297
fmt.Printf(" %s skills list-builtin\n", commandName)
22842298
fmt.Printf(" %s skills remove weather\n", commandName)
@@ -2307,7 +2321,7 @@ func skillsInstallCmd(installer *skills.SkillInstaller) {
23072321
if len(os.Args) < 4 {
23082322
commandName := invokedCLIName()
23092323
fmt.Printf("Usage: %s skills install <github-repo>\n", commandName)
2310-
fmt.Printf("Example: %s skills install sipeed/picoclaw-skills/weather\n", commandName)
2324+
fmt.Printf("Example: %s skills install drpedapati/sciclaw-skills/weather\n", commandName)
23112325
return
23122326
}
23132327

cmd/picoclaw/tui/model.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import (
1010
"github.com/charmbracelet/lipgloss"
1111
)
1212

13+
// Version is set by main.init() before the TUI launches.
14+
var Version string
15+
1316
// Tab logical IDs — stable regardless of which tabs are visible.
1417
const (
1518
tabHome = 0
@@ -446,11 +449,22 @@ func (m Model) View() string {
446449
var b strings.Builder
447450

448451
// Header
449-
title := "🦞🧪 sciClaw Control Center"
452+
ver := Version
453+
if ver == "" {
454+
ver = "dev"
455+
}
456+
title := fmt.Sprintf("🦞🧪 sciClaw v%s", ver)
450457
if m.exec.Mode() == ModeVM {
451-
title = "🦞🧪 sciClaw VM Control Center"
458+
title = fmt.Sprintf("🦞🧪 sciClaw VM v%s", ver)
452459
}
453460
header := lipgloss.NewStyle().Bold(true).Foreground(colorAccent).Render(title)
461+
if m.snapshot != nil && m.snapshot.GatewayVersion != "" &&
462+
m.snapshot.GatewayVersion != ver &&
463+
m.snapshot.ServiceRunning &&
464+
ver != "dev" && m.snapshot.GatewayVersion != "dev" {
465+
mismatch := fmt.Sprintf(" ⚠ Gateway running v%s — restart service to update", m.snapshot.GatewayVersion)
466+
header += lipgloss.NewStyle().Foreground(colorWarning).Render(mismatch)
467+
}
454468
b.WriteString(header)
455469
b.WriteString("\n")
456470

cmd/picoclaw/tui/snapshot.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type VMSnapshot struct {
1919

2020
// Agent version
2121
AgentVersion string
22+
// Gateway version from ~/.picoclaw/gateway.status.json
23+
GatewayVersion string
2224

2325
// Config/workspace state
2426
ConfigExists bool
@@ -139,12 +141,13 @@ func collectVMSnapshot(exec Executor) VMSnapshot {
139141
var cfgRaw, authRaw, hostCfgRaw string
140142
var cfgErr, authErr, hostCfgErr error
141143

142-
wg.Add(5)
144+
wg.Add(6)
143145
go func() { defer wg.Done(); vmInfo = GetVMInfo() }()
144146
go func() { defer wg.Done(); cfgRaw, cfgErr = exec.ReadFile(exec.ConfigPath()) }()
145147
go func() { defer wg.Done(); authRaw, authErr = exec.ReadFile(exec.AuthPath()) }()
146148
go func() { defer wg.Done(); hostCfgRaw, hostCfgErr = hostConfigRaw() }()
147149
go func() { defer wg.Done(); snap.AgentVersion = exec.AgentVersion() }()
150+
go func() { defer wg.Done(); snap.GatewayVersion = readGatewayVersion(exec) }()
148151
wg.Wait()
149152

150153
snap.State = vmInfo.State
@@ -215,10 +218,11 @@ func collectLocalSnapshot(exec Executor) VMSnapshot {
215218
var cfgRaw, authRaw string
216219
var cfgErr, authErr error
217220

218-
wg.Add(3)
221+
wg.Add(4)
219222
go func() { defer wg.Done(); cfgRaw, cfgErr = exec.ReadFile(exec.ConfigPath()) }()
220223
go func() { defer wg.Done(); authRaw, authErr = exec.ReadFile(exec.AuthPath()) }()
221224
go func() { defer wg.Done(); snap.AgentVersion = exec.AgentVersion() }()
225+
go func() { defer wg.Done(); snap.GatewayVersion = readGatewayVersion(exec) }()
222226
wg.Wait()
223227

224228
// Parse config.
@@ -262,6 +266,21 @@ func collectLocalSnapshot(exec Executor) VMSnapshot {
262266
return snap
263267
}
264268

269+
func readGatewayVersion(exec Executor) string {
270+
path := filepath.Join(filepath.Dir(exec.ConfigPath()), "gateway.status.json")
271+
data, err := os.ReadFile(path)
272+
if err != nil {
273+
return ""
274+
}
275+
var status struct {
276+
Version string `json:"version"`
277+
}
278+
if json.Unmarshal(data, &status) != nil {
279+
return ""
280+
}
281+
return status.Version
282+
}
283+
265284
func providerState(prov providerJSON, cred authCredJSON) string {
266285
if strings.TrimSpace(prov.APIKey) != "" {
267286
return "ready"

0 commit comments

Comments
 (0)