Skip to content

Commit 9e71cf1

Browse files
authored
Merge pull request #2 from jamesrossdev/feat/phase-12-config-reset
Feat/phase 12 implementation.
2 parents 5756101 + 8e4648a commit 9e71cf1

17 files changed

Lines changed: 3938 additions & 137 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ tasks/
4545

4646
# Added by goreleaser init:
4747
dist/
48+
IMPROVEMENTS.md

AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ Every byte counts. The binary is already ~15MB stripped. Adding dependencies inc
7272
### 7. AI Agent Access to the Device
7373
If you are an AI agent and need to test changes, examine logs, or execute commands directly on the Luckfox Pico hardware, **do not guess the IP or password**. Simply ask the user to provide the SSH IP address and password for the device, and use the `run_command` tool via `sshpass` (e.g., `sshpass -p <password> ssh root@<ip>`).
7474

75+
### 8. Committing and Pushing Code
76+
If you are an AI agent, you **MUST NEVER** commit or push code without explicit permission from the user. When you are asked to commit, you must ensure that the tracked `firmware/overlay` directory is completely up to date with whatever modifications were made inside the untracked `luckfox-pico-sdk` directory. This is the only way secondary developers receive OS-level modifications.
77+
78+
### 9. Execution Requires Approved Implementation Plan
79+
If you are an AI agent, you **MUST NEVER** execute code changes, environment modifications, or configuration adjustments without explicitly drafting an implementation plan and receiving the user's explicit approval first. Do not make unauthorized technical assumptions.
80+
81+
### 10. Multiple Daemon Instances & PID Tracking
82+
If `luckyclaw gateway -b` is executed while a daemon started by `/etc/init.d/S99luckyclaw` is already running it will overwrite the `/var/run/luckyclaw.pid` file. Because the init script only tracks the latest PID, subsequent `stop` or `restart` commands will leave the original daemon alive as a zombie, causing duplicate Telegram processing and hallucinated timestamps in session memory. **Fix:** Going forward, making sure we strictly append `&& killall -9 luckyclaw` alongside the init script (which I've started doing in my deploy commands) completely eliminates the possibility of this happening again.
83+
7584
## Build & Deploy
7685

7786
### Testing Before Commits

CULLED.md

Lines changed: 0 additions & 44 deletions
This file was deleted.

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
1111
GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev")
1212
BUILD_TIME=$(shell date +%FT%T%z)
1313
GO_VERSION=$(shell $(GO) version | awk '{print $$3}')
14-
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION)"
14+
LDFLAGS=-ldflags "-s -w -X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION)"
1515

1616
# Go variables
1717
GO?=go
18-
GOFLAGS?=-v
18+
GOFLAGS?=-v -trimpath
1919

2020
# Installation
2121
INSTALL_PREFIX?=$(HOME)/.local

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ The wizard walks you through:
8686

8787
1. **API Provider** — OpenRouter - but you can manually set up OpenAI, Anthropic, Ollama and others in config.json
8888
2. **API Key** — Paste your key, it's validated in real-time
89-
3. **Timezone**Auto-detected via IP, or enter manually
90-
4. **Messaging** — Optionally set up Telegram (Discord, WhatsApp, and otherscoming soon)
89+
3. **Timezone**Explicitly enter your IANA Zone classification via the [Wikipedia TZ Database List](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)
90+
4. **Messaging** — Optionally set up Telegram (Discord, WhatsApp, and others coming soon)
9191
5. **Start gateway** — Optionally start the AI gateway in the background
9292

9393
### 4. Chat!

cmd/luckyclaw/main.go

Lines changed: 110 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ func main() {
217217
}
218218
case "version", "--version", "-v":
219219
printVersion()
220+
case "help", "--help", "-h":
221+
printHelp()
222+
case "config-reset":
223+
configResetCmd()
220224
default:
221225
fmt.Printf("Unknown command: %s\n", command)
222226
printHelp()
@@ -229,17 +233,19 @@ func printHelp() {
229233
fmt.Println("Usage: luckyclaw <command>")
230234
fmt.Println()
231235
fmt.Println("Commands:")
232-
fmt.Println(" onboard Initialize luckyclaw configuration and workspace")
233-
fmt.Println(" agent Interact with the agent directly")
234-
fmt.Println(" gateway Start luckyclaw gateway (-b for background)")
235-
fmt.Println(" stop Stop running gateway")
236-
fmt.Println(" restart Restart gateway (stop + start in background)")
237-
fmt.Println(" status Show luckyclaw status")
238-
fmt.Println(" cron Manage scheduled tasks")
239-
fmt.Println(" auth Manage authentication (login, logout, status)")
240-
fmt.Println(" migrate Migrate from OpenClaw to LuckyClaw")
241-
fmt.Println(" skills Manage skills (install, list, remove)")
242-
fmt.Println(" version Show version information")
236+
fmt.Println(" onboard Initialize luckyclaw configuration and workspace")
237+
fmt.Println(" agent Interact with the agent directly")
238+
fmt.Println(" gateway Start luckyclaw gateway (-b for background)")
239+
fmt.Println(" stop Stop running gateway")
240+
fmt.Println(" restart Restart gateway (stop + start in background)")
241+
fmt.Println(" status Show luckyclaw status")
242+
fmt.Println(" cron Manage scheduled tasks")
243+
fmt.Println(" auth Manage authentication (login, logout, status)")
244+
fmt.Println(" migrate Migrate from OpenClaw to LuckyClaw")
245+
fmt.Println(" skills Manage skills (install, list, remove)")
246+
fmt.Println(" config-reset Safely delete your config.json (API keys/models)")
247+
fmt.Println(" version Show version information")
248+
fmt.Println(" help Show this help message")
243249
}
244250

245251
// promptLine reads a single line from stdin with a prompt.
@@ -299,22 +305,6 @@ func validateTelegramToken(token string) (string, error) {
299305
return result.Result.Username, nil
300306
}
301307

302-
// detectTimezone tries to auto-detect timezone via IP geolocation.
303-
func detectTimezone() string {
304-
client := &http.Client{Timeout: 5 * time.Second}
305-
resp, err := client.Get("http://ip-api.com/json/?fields=timezone")
306-
if err != nil {
307-
return ""
308-
}
309-
defer resp.Body.Close()
310-
311-
var result struct {
312-
Timezone string `json:"timezone"`
313-
}
314-
json.NewDecoder(resp.Body).Decode(&result)
315-
return result.Timezone
316-
}
317-
318308
// detectBoardModel reads the device tree model string.
319309
func detectBoardModel() string {
320310
data, err := os.ReadFile("/proc/device-tree/model")
@@ -401,16 +391,10 @@ func onboard() {
401391
fmt.Println()
402392
fmt.Println(" Step 3: Timezone")
403393
fmt.Println(" ─────────────────")
404-
detectedTZ := detectTimezone()
405-
if detectedTZ != "" {
406-
fmt.Printf(" Detected: %s\n", detectedTZ)
407-
if !promptYN(" Use this timezone?") {
408-
detectedTZ = promptLine(" Enter timezone (e.g. America/New_York): ")
409-
}
410-
} else {
411-
fmt.Println(" Could not auto-detect timezone.")
412-
detectedTZ = promptLine(" Enter timezone (e.g. America/New_York): ")
413-
}
394+
fmt.Println(" Find your Timezone code here:")
395+
fmt.Println(" https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List")
396+
fmt.Println()
397+
detectedTZ := promptLine(" Enter timezone (e.g. Europe/London): ")
414398
if detectedTZ != "" {
415399
// Calculate explicit offset for config
416400
if loc, err := time.LoadLocation(detectedTZ); err == nil {
@@ -494,7 +478,7 @@ func onboard() {
494478

495479
fmt.Println()
496480
fmt.Println(" ╔══════════════════════════════════════╗")
497-
fmt.Println(" ║ 🦞 LuckyClaw is ready! ║")
481+
fmt.Println(" ║ 🦞 LuckyClaw is ready! ║")
498482
fmt.Println(" ╚══════════════════════════════════════╝")
499483
fmt.Println()
500484
fmt.Println(" Commands:")
@@ -503,6 +487,7 @@ func onboard() {
503487
fmt.Println(" luckyclaw gateway -b — Start in background")
504488
fmt.Println(" luckyclaw stop — Stop the gateway")
505489
fmt.Println(" luckyclaw restart — Restart the gateway")
490+
fmt.Println(" luckyclaw help — View more commands")
506491
fmt.Println()
507492
}
508493

@@ -651,6 +636,35 @@ func copyEmbeddedToTarget(targetDir string) error {
651636
return err
652637
}
653638

639+
func configResetCmd() {
640+
configPath := getConfigPath()
641+
642+
fmt.Println()
643+
fmt.Println(" ⚠️ WARNING: You are about to erase LuckyClaw's configuration. ⚠️")
644+
fmt.Println(" This will completely delete your API keys, LLM preferences,")
645+
fmt.Println(" and custom timezone. (It will NOT delete conversation memory).")
646+
fmt.Println()
647+
648+
confirm := promptLine(" Type 'RESET' to confirm: ")
649+
if confirm != "RESET" {
650+
fmt.Println(" Config reset cancelled.")
651+
return
652+
}
653+
654+
err := os.Remove(configPath)
655+
if err != nil {
656+
if os.IsNotExist(err) {
657+
fmt.Println("\n No config file found (already reset).")
658+
} else {
659+
fmt.Printf("\n ✗ Failed to delete config file: %v\n", err)
660+
}
661+
return
662+
}
663+
664+
fmt.Println("\n ✓ Config successfully erased.")
665+
fmt.Println(" Run 'luckyclaw onboard' to start fresh.")
666+
}
667+
654668
func createWorkspaceTemplates(workspace string) {
655669
err := copyEmbeddedToTarget(workspace)
656670
if err != nil {
@@ -925,7 +939,7 @@ func gatewayCmd() {
925939
})
926940

927941
// Setup cron tool and service
928-
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace)
942+
cronService := setupCronTool(agentLoop, msgBus, cfg)
929943

930944
heartbeatService := heartbeat.NewHeartbeatService(
931945
cfg.WorkspacePath(),
@@ -1418,18 +1432,29 @@ func authStatusCmd() {
14181432
}
14191433

14201434
func getConfigPath() string {
1435+
if envPath := os.Getenv("LUCKYCLAW_CONFIG"); envPath != "" {
1436+
return envPath
1437+
}
1438+
if _, err := os.Stat("/oem"); err == nil {
1439+
return "/oem/.luckyclaw/config.json"
1440+
}
14211441
home, _ := os.UserHomeDir()
14221442
return filepath.Join(home, ".luckyclaw", "config.json")
14231443
}
14241444

1425-
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool) *cron.CronService {
1426-
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
1445+
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, cfg *config.Config) *cron.CronService {
1446+
cronStorePath := filepath.Join(cfg.WorkspacePath(), "cron", "jobs.json")
1447+
1448+
loc, err := time.LoadLocation(cfg.Gateway.TimezoneName)
1449+
if err != nil {
1450+
loc = time.UTC
1451+
}
14271452

14281453
// Create cron service
1429-
cronService := cron.NewCronService(cronStorePath, nil)
1454+
cronService := cron.NewCronService(cronStorePath, nil, loc)
14301455

14311456
// Create and register CronTool
1432-
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict)
1457+
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace)
14331458
agentLoop.RegisterTool(cronTool)
14341459

14351460
// Set the onJob handler
@@ -1442,9 +1467,32 @@ func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace
14421467
}
14431468

14441469
func loadConfig() (*config.Config, error) {
1445-
return config.LoadConfig(getConfigPath())
1470+
cfg, err := config.LoadConfig(getConfigPath())
1471+
if err == nil {
1472+
applyConfigDefaults(cfg)
1473+
}
1474+
return cfg, err
14461475
}
14471476

1477+
func applyConfigDefaults(cfg *config.Config) {
1478+
changed := false
1479+
defaults := config.DefaultConfig()
1480+
if cfg.Agents.Defaults.MaxTokens < defaults.Agents.Defaults.MaxTokens {
1481+
cfg.Agents.Defaults.MaxTokens = defaults.Agents.Defaults.MaxTokens
1482+
changed = true
1483+
}
1484+
if cfg.Agents.Defaults.MaxToolIterations < defaults.Agents.Defaults.MaxToolIterations {
1485+
cfg.Agents.Defaults.MaxToolIterations = defaults.Agents.Defaults.MaxToolIterations
1486+
changed = true
1487+
}
1488+
if changed {
1489+
logger.Info("Auto-upgrading stale memory configuration limits to firmware defaults")
1490+
err := config.SaveConfig(getConfigPath(), cfg)
1491+
if err != nil {
1492+
logger.Error(fmt.Sprintf("Failed to save auto-upgraded config: %v", err))
1493+
}
1494+
}
1495+
}
14481496
func cronCmd() {
14491497
if len(os.Args) < 3 {
14501498
cronHelp()
@@ -1462,21 +1510,26 @@ func cronCmd() {
14621510

14631511
cronStorePath := filepath.Join(cfg.WorkspacePath(), "cron", "jobs.json")
14641512

1513+
loc, err := time.LoadLocation(cfg.Gateway.TimezoneName)
1514+
if err != nil {
1515+
loc = time.UTC
1516+
}
1517+
14651518
switch subcommand {
14661519
case "list":
1467-
cronListCmd(cronStorePath)
1520+
cronListCmd(cronStorePath, loc)
14681521
case "add":
1469-
cronAddCmd(cronStorePath)
1522+
cronAddCmd(cronStorePath, loc)
14701523
case "remove":
14711524
if len(os.Args) < 4 {
14721525
fmt.Println("Usage: luckyclaw cron remove <job_id>")
14731526
return
14741527
}
1475-
cronRemoveCmd(cronStorePath, os.Args[3])
1528+
cronRemoveCmd(cronStorePath, os.Args[3], loc)
14761529
case "enable":
1477-
cronEnableCmd(cronStorePath, false)
1530+
cronEnableCmd(cronStorePath, false, loc)
14781531
case "disable":
1479-
cronEnableCmd(cronStorePath, true)
1532+
cronEnableCmd(cronStorePath, true, loc)
14801533
default:
14811534
fmt.Printf("Unknown cron command: %s\n", subcommand)
14821535
cronHelp()
@@ -1501,8 +1554,8 @@ func cronHelp() {
15011554
fmt.Println(" --channel Channel for delivery")
15021555
}
15031556

1504-
func cronListCmd(storePath string) {
1505-
cs := cron.NewCronService(storePath, nil)
1557+
func cronListCmd(storePath string, loc *time.Location) {
1558+
cs := cron.NewCronService(storePath, nil, loc)
15061559
jobs := cs.ListJobs(true) // Show all jobs, including disabled
15071560

15081561
if len(jobs) == 0 {
@@ -1540,7 +1593,7 @@ func cronListCmd(storePath string) {
15401593
}
15411594
}
15421595

1543-
func cronAddCmd(storePath string) {
1596+
func cronAddCmd(storePath string, loc *time.Location) {
15441597
name := ""
15451598
message := ""
15461599
var everySec *int64
@@ -1618,7 +1671,7 @@ func cronAddCmd(storePath string) {
16181671
}
16191672
}
16201673

1621-
cs := cron.NewCronService(storePath, nil)
1674+
cs := cron.NewCronService(storePath, nil, loc)
16221675
job, err := cs.AddJob(name, schedule, message, deliver, channel, to)
16231676
if err != nil {
16241677
fmt.Printf("Error adding job: %v\n", err)
@@ -1628,23 +1681,23 @@ func cronAddCmd(storePath string) {
16281681
fmt.Printf("✓ Added job '%s' (%s)\n", job.Name, job.ID)
16291682
}
16301683

1631-
func cronRemoveCmd(storePath, jobID string) {
1632-
cs := cron.NewCronService(storePath, nil)
1684+
func cronRemoveCmd(storePath, jobID string, loc *time.Location) {
1685+
cs := cron.NewCronService(storePath, nil, loc)
16331686
if cs.RemoveJob(jobID) {
16341687
fmt.Printf("✓ Removed job %s\n", jobID)
16351688
} else {
16361689
fmt.Printf("✗ Job %s not found\n", jobID)
16371690
}
16381691
}
16391692

1640-
func cronEnableCmd(storePath string, disable bool) {
1693+
func cronEnableCmd(storePath string, disable bool, loc *time.Location) {
16411694
if len(os.Args) < 4 {
16421695
fmt.Println("Usage: luckyclaw cron enable/disable <job_id>")
16431696
return
16441697
}
16451698

16461699
jobID := os.Args[3]
1647-
cs := cron.NewCronService(storePath, nil)
1700+
cs := cron.NewCronService(storePath, nil, loc)
16481701
enabled := !disable
16491702

16501703
job := cs.EnableJob(jobID, enabled)

0 commit comments

Comments
 (0)