chore: update Makefile, .gitignore, ROADMAP, and wire XDG in main.go
Makefile: add eval targets, update build/test targets for new layout .gitignore: add eval fixture outputs and config overrides ROADMAP.md: add IdentitySync, XDG migration, and eval suite milestones cmd/picoclaw/main.go: - Use config.SkillsDir() / config.ConfigDir() for skills subcommands - Call migrate.MigrateToXDG() during onboard - createWorkspaceTemplates takes *Config (uses XDG-aware seed functions) - seedEmbeddedIdentity / seedEmbeddedSkills replace copyEmbeddedToTarget
This commit is contained in:
parent
5d5ae0dba3
commit
3130540ab4
4 changed files with 192 additions and 153 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -43,4 +43,3 @@ tasks/
|
||||||
|
|
||||||
# Added by goreleaser init:
|
# Added by goreleaser init:
|
||||||
dist/
|
dist/
|
||||||
eval-runner
|
|
||||||
|
|
|
||||||
26
Makefile
26
Makefile
|
|
@ -222,13 +222,31 @@ eval-build: generate
|
||||||
@$(GO) build $(GOFLAGS) $(LDFLAGS) -o eval/bin/eval-runner ./eval/cmd/eval-runner
|
@$(GO) build $(GOFLAGS) $(LDFLAGS) -o eval/bin/eval-runner ./eval/cmd/eval-runner
|
||||||
@echo "Eval runner built: eval/bin/eval-runner"
|
@echo "Eval runner built: eval/bin/eval-runner"
|
||||||
|
|
||||||
## eval: Run the eval suite against the current build
|
## eval: Run the eval suite (default config only) against the current build
|
||||||
eval: eval-build
|
eval: eval-build eval-fixtures
|
||||||
@echo "Running eval suite..."
|
@echo "Running eval suite (default config)..."
|
||||||
@cd eval && npx promptfoo eval --no-progress-bar
|
@cd eval && npx promptfoo eval --config promptfooconfig-default.yaml --no-progress-bar
|
||||||
@echo "Results: eval/results/latest.json"
|
@echo "Results: eval/results/latest.json"
|
||||||
@echo "View: cd eval && npx promptfoo view"
|
@echo "View: cd eval && npx promptfoo view"
|
||||||
|
|
||||||
|
## eval-matrix: Run eval suite against all config variants (default, progressive, no-memory)
|
||||||
|
eval-matrix: eval-build eval-fixtures
|
||||||
|
@echo "Running eval matrix (all config variants)..."
|
||||||
|
@cd eval && npx promptfoo eval --config promptfooconfig.yaml --no-progress-bar
|
||||||
|
@echo "Results: eval/results/latest.json"
|
||||||
|
@echo "View: cd eval && npx promptfoo view"
|
||||||
|
|
||||||
|
## eval-fixtures: Reset workspace to a known state and seed fixture files for eval
|
||||||
|
eval-fixtures:
|
||||||
|
@mkdir -p $(HOME)/.picoclaw/workspace
|
||||||
|
@rm -f $(HOME)/.picoclaw/workspace/eval_test_output.txt \
|
||||||
|
$(HOME)/.picoclaw/workspace/test_steps.txt \
|
||||||
|
$(HOME)/.picoclaw/workspace/eval_checkpoint.txt
|
||||||
|
@printf 'picoclaw eval fixture — hello from the eval harness\nThis is line two of the fixture file.\n' > $(HOME)/.picoclaw/workspace/eval_fixture.txt
|
||||||
|
@cp -f eval/fixtures/sample_data.txt $(HOME)/.picoclaw/workspace/sample_data.txt
|
||||||
|
@mkdir -p $(HOME)/.picoclaw/workspace/skills
|
||||||
|
@cp -rf eval/fixtures/skills/* $(HOME)/.picoclaw/workspace/skills/ 2>/dev/null || true
|
||||||
|
|
||||||
## eval-view: Open the promptfoo results viewer
|
## eval-view: Open the promptfoo results viewer
|
||||||
eval-view:
|
eval-view:
|
||||||
@cd eval && npx promptfoo view
|
@cd eval && npx promptfoo view
|
||||||
|
|
|
||||||
27
ROADMAP.md
27
ROADMAP.md
|
|
@ -229,6 +229,33 @@ flowchart LR
|
||||||
## Ideas and improvements
|
## Ideas and improvements
|
||||||
|
|
||||||
- [ ] Add a new tool for the agent to use: `focus_search`
|
- [ ] Add a new tool for the agent to use: `focus_search`
|
||||||
|
- [ ] Clean up the main.go and extract to modules
|
||||||
|
- [ ] Create a pure Application API that I/O calls into
|
||||||
|
- [ ] cli
|
||||||
|
- [ ] daemon
|
||||||
|
- [ ] web
|
||||||
|
- [ ] grpc
|
||||||
|
- [ ] http
|
||||||
|
- [ ] websocket
|
||||||
|
- [ ] tcp
|
||||||
|
- [ ] udp
|
||||||
|
- [ ] serial
|
||||||
|
- [ ] i2c
|
||||||
|
- [ ] spi
|
||||||
|
- [ ] pwm
|
||||||
|
- [ ] etc
|
||||||
|
- [ ] all of these should be able to be configured and plugged in/out at runtime
|
||||||
|
- [ ] Migrate to Cobra CLI framework
|
||||||
|
- [ ] Use command-palette pattern for subcommands
|
||||||
|
- [ ] keep cli commands as pure cli that calls into the application
|
||||||
|
- [ ] Consolidate tool signatures:
|
||||||
|
- [ ] fold tools that operate on the same data into a single tool with a "mode"/"action"/"event" parameter
|
||||||
|
- [ ] move the "mode"/"action"/"event" parameter to the beginning of the tool signature
|
||||||
|
- [ ] add a "description" field to the tool signature
|
||||||
|
- [ ] this reduces the number of tools and makes the tool signatures more consistent
|
||||||
|
- [ ] Tools can then live in packages like `pkg/tools/filesystem.go` and `pkg/tools/network.go` where we expose only a limited ABI/API to the agent loop
|
||||||
|
- [ ] Extract out all inline prompts into separate files
|
||||||
|
- [ ] Use dotprompt, poml, or any other structured prompt builder to build prompts
|
||||||
- [ ] Isolated tool runtime + DAG executor + RLM engine — see [ADR-001](docs/adr/001-isolated-tool-runtime.md)
|
- [ ] Isolated tool runtime + DAG executor + RLM engine — see [ADR-001](docs/adr/001-isolated-tool-runtime.md)
|
||||||
- [ ] Layer 1: Capability manifests (`CapableTool` interface)
|
- [ ] Layer 1: Capability manifests (`CapableTool` interface)
|
||||||
- [ ] Layer 2: SecureBus + FlatBuffers command protocol (incl. DAG types) + leak scanning
|
- [ ] Layer 2: SecureBus + FlatBuffers command protocol (incl. DAG types) + leak scanning
|
||||||
|
|
|
||||||
|
|
@ -155,19 +155,12 @@ func main() {
|
||||||
|
|
||||||
subcommand := os.Args[2]
|
subcommand := os.Args[2]
|
||||||
|
|
||||||
cfg, err := loadConfig()
|
skillsDir, _ := config.SkillsDir()
|
||||||
if err != nil {
|
installer := skills.NewSkillInstaller(skillsDir)
|
||||||
fmt.Printf("Error loading config: %v\n", err)
|
cfgDir, _ := config.ConfigDir()
|
||||||
os.Exit(1)
|
globalSkillsDir := filepath.Join(cfgDir, "skills")
|
||||||
}
|
builtinSkillsDir := filepath.Join(cfgDir, "picoclaw", "skills")
|
||||||
|
skillsLoader := skills.NewSkillsLoader(skillsDir, globalSkillsDir, builtinSkillsDir)
|
||||||
workspace := cfg.WorkspacePath()
|
|
||||||
installer := skills.NewSkillInstaller(workspace)
|
|
||||||
// 获取全局配置目录和内置 skills 目录
|
|
||||||
globalDir := filepath.Dir(getConfigPath())
|
|
||||||
globalSkillsDir := filepath.Join(globalDir, "skills")
|
|
||||||
builtinSkillsDir := filepath.Join(globalDir, "picoclaw", "skills")
|
|
||||||
skillsLoader := skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir)
|
|
||||||
|
|
||||||
switch subcommand {
|
switch subcommand {
|
||||||
case "list":
|
case "list":
|
||||||
|
|
@ -181,7 +174,7 @@ func main() {
|
||||||
}
|
}
|
||||||
skillsRemoveCmd(installer, os.Args[3])
|
skillsRemoveCmd(installer, os.Args[3])
|
||||||
case "install-builtin":
|
case "install-builtin":
|
||||||
skillsInstallBuiltinCmd(workspace)
|
skillsInstallBuiltinCmd(skillsDir)
|
||||||
case "list-builtin":
|
case "list-builtin":
|
||||||
skillsListBuiltinCmd()
|
skillsListBuiltinCmd()
|
||||||
case "search":
|
case "search":
|
||||||
|
|
@ -250,8 +243,11 @@ func onboard() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace := cfg.WorkspacePath()
|
if migErr := migrate.MigrateToXDG(""); migErr != nil {
|
||||||
createWorkspaceTemplates(workspace)
|
fmt.Printf("Warning: XDG migration failed: %v\n", migErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
createWorkspaceTemplates(cfg)
|
||||||
|
|
||||||
fmt.Printf("%s picoclaw is ready!\n", logo)
|
fmt.Printf("%s picoclaw is ready!\n", logo)
|
||||||
|
|
||||||
|
|
@ -280,58 +276,68 @@ func onboard() {
|
||||||
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
|
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyEmbeddedToTarget(targetDir string) error {
|
// seedEmbeddedIdentity copies identity template files from the embedded FS
|
||||||
// Ensure target directory exists
|
// into the XDG identity directory ($XDG_CONFIG_HOME/picoclaw/identity/).
|
||||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
func seedEmbeddedIdentity(identityDir string) error {
|
||||||
return fmt.Errorf("Failed to create target directory: %w", err)
|
identityFiles := []string{"AGENT.md", "IDENTITY.md", "SOUL.md", "USER.md"}
|
||||||
}
|
for _, name := range identityFiles {
|
||||||
|
data, err := embeddedFiles.ReadFile("workspace/" + name)
|
||||||
// Walk through all files in embed.FS
|
|
||||||
err := fs.WalkDir(embeddedFiles, "workspace", func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
continue
|
||||||
}
|
}
|
||||||
|
targetPath := filepath.Join(identityDir, name)
|
||||||
// Skip directories
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
if d.IsDir() {
|
continue
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read embedded file
|
|
||||||
data, err := embeddedFiles.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to read embedded file %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
new_path, err := filepath.Rel("workspace", path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to get relative path for %s: %v\n", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build target file path
|
|
||||||
targetPath := filepath.Join(targetDir, new_path)
|
|
||||||
|
|
||||||
// Ensure target file's directory exists
|
|
||||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
|
||||||
return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write file
|
|
||||||
if err := os.WriteFile(targetPath, data, 0644); err != nil {
|
if err := os.WriteFile(targetPath, data, 0644); err != nil {
|
||||||
return fmt.Errorf("Failed to write file %s: %w", targetPath, err)
|
return fmt.Errorf("write %s: %w", targetPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// seedEmbeddedSkills copies skill templates from the embedded FS into the
|
||||||
})
|
// XDG skills directory ($XDG_DATA_HOME/picoclaw/skills/).
|
||||||
|
func seedEmbeddedSkills(skillsDir string) error {
|
||||||
|
return fs.WalkDir(embeddedFiles, "workspace/skills", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
data, readErr := embeddedFiles.ReadFile(path)
|
||||||
func createWorkspaceTemplates(workspace string) {
|
if readErr != nil {
|
||||||
err := copyEmbeddedToTarget(workspace)
|
return readErr
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error copying workspace templates: %v\n", err)
|
|
||||||
}
|
}
|
||||||
|
relPath, _ := filepath.Rel("workspace/skills", path)
|
||||||
|
targetPath := filepath.Join(skillsDir, relPath)
|
||||||
|
if _, statErr := os.Stat(targetPath); statErr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
os.MkdirAll(filepath.Dir(targetPath), 0755)
|
||||||
|
return os.WriteFile(targetPath, data, 0644)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createWorkspaceTemplates(cfg *config.Config) {
|
||||||
|
identityDir, err := config.IdentityDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error resolving identity dir: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := seedEmbeddedIdentity(identityDir); err != nil {
|
||||||
|
fmt.Printf("Error seeding identity files: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
skillsDir, err := config.SkillsDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error resolving skills dir: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := seedEmbeddedSkills(skillsDir); err != nil {
|
||||||
|
fmt.Printf("Error seeding skills: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure sandbox directory exists.
|
||||||
|
_ = os.MkdirAll(cfg.SandboxPath(), 0755)
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateCmd() {
|
func migrateCmd() {
|
||||||
|
|
@ -439,21 +445,25 @@ func agentCmd() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
appCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
languageModel, err := fantasyProvider.LanguageModel(ctx, picofantasy.ModelID(cfg))
|
defer stop()
|
||||||
|
|
||||||
|
languageModel, err := fantasyProvider.LanguageModel(appCtx, picofantasy.ModelID(cfg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error creating language model: %v\n", err)
|
fmt.Printf("Error creating language model: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
agentLoop := agent.NewAgentLoop(cfg, msgBus, languageModel)
|
agentLoop, err := agent.NewAgentLoop(appCtx, cfg, msgBus, languageModel)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error initializing agent: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// Wire ITR SecureBus — routes all tool calls through capability enforcement.
|
|
||||||
closeBus := setupSecureBus(agentLoop)
|
closeBus := setupSecureBus(agentLoop)
|
||||||
defer closeBus()
|
defer closeBus()
|
||||||
|
|
||||||
// Print agent startup info (only for interactive mode)
|
|
||||||
startupInfo := agentLoop.GetStartupInfo()
|
startupInfo := agentLoop.GetStartupInfo()
|
||||||
logger.InfoCF("agent", "Agent initialized",
|
logger.InfoCF("agent", "Agent initialized",
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
|
|
@ -463,7 +473,7 @@ func agentCmd() {
|
||||||
})
|
})
|
||||||
|
|
||||||
if message != "" {
|
if message != "" {
|
||||||
response, err := agentLoop.ProcessDirect(ctx, message, sessionKey)
|
response, err := agentLoop.ProcessDirect(appCtx, message, sessionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
fmt.Printf("Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -471,11 +481,11 @@ func agentCmd() {
|
||||||
fmt.Printf("\n%s %s\n", logo, response)
|
fmt.Printf("\n%s %s\n", logo, response)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", logo)
|
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", logo)
|
||||||
interactiveMode(agentLoop, sessionKey)
|
interactiveMode(appCtx, agentLoop, sessionKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
func interactiveMode(ctx context.Context, agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
prompt := fmt.Sprintf("%s You: ", logo)
|
prompt := fmt.Sprintf("%s You: ", logo)
|
||||||
|
|
||||||
rl, err := readline.NewEx(&readline.Config{
|
rl, err := readline.NewEx(&readline.Config{
|
||||||
|
|
@ -489,7 +499,7 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error initializing readline: %v\n", err)
|
fmt.Printf("Error initializing readline: %v\n", err)
|
||||||
fmt.Println("Falling back to simple input mode...")
|
fmt.Println("Falling back to simple input mode...")
|
||||||
simpleInteractiveMode(agentLoop, sessionKey)
|
simpleInteractiveMode(ctx, agentLoop, sessionKey)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rl.Close()
|
defer rl.Close()
|
||||||
|
|
@ -515,7 +525,6 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
|
@ -526,10 +535,10 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
func simpleInteractiveMode(ctx context.Context, agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
for {
|
for {
|
||||||
fmt.Print(fmt.Sprintf("%s You: ", logo))
|
fmt.Printf("%s You: ", logo)
|
||||||
line, err := reader.ReadString('\n')
|
line, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
|
|
@ -550,7 +559,6 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
|
@ -584,20 +592,25 @@ func gatewayCmd() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
languageModel, err := fantasyProvider.LanguageModel(context.Background(), picofantasy.ModelID(cfg))
|
appCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
languageModel, err := fantasyProvider.LanguageModel(appCtx, picofantasy.ModelID(cfg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error creating language model: %v\n", err)
|
fmt.Printf("Error creating language model: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
agentLoop := agent.NewAgentLoop(cfg, msgBus, languageModel)
|
agentLoop, err := agent.NewAgentLoop(appCtx, cfg, msgBus, languageModel)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error initializing agent: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// Wire ITR SecureBus — routes all tool calls through capability enforcement.
|
|
||||||
closeBus := setupSecureBus(agentLoop)
|
closeBus := setupSecureBus(agentLoop)
|
||||||
defer closeBus()
|
defer closeBus()
|
||||||
|
|
||||||
// Print agent startup info
|
|
||||||
fmt.Println("\n📦 Agent Status:")
|
fmt.Println("\n📦 Agent Status:")
|
||||||
startupInfo := agentLoop.GetStartupInfo()
|
startupInfo := agentLoop.GetStartupInfo()
|
||||||
toolsInfo := startupInfo["tools"].(map[string]interface{})
|
toolsInfo := startupInfo["tools"].(map[string]interface{})
|
||||||
|
|
@ -621,10 +634,10 @@ func gatewayCmd() {
|
||||||
if del := agentLoop.MemoryDelegate(); del != nil {
|
if del := agentLoop.MemoryDelegate(); del != nil {
|
||||||
cronOpts = append(cronOpts, cron.WithCronDelegate(del, "picoclaw"))
|
cronOpts = append(cronOpts, cron.WithCronDelegate(del, "picoclaw"))
|
||||||
}
|
}
|
||||||
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cronOpts...)
|
cronService := setupCronTool(appCtx, agentLoop, msgBus, cfg.SandboxPath(), cfg.RestrictToSandbox(), execTimeout, cronOpts...)
|
||||||
|
|
||||||
heartbeatService := heartbeat.NewHeartbeatService(
|
heartbeatService := heartbeat.NewHeartbeatService(
|
||||||
cfg.WorkspacePath(),
|
cfg.SandboxPath(),
|
||||||
cfg.Heartbeat.Interval,
|
cfg.Heartbeat.Interval,
|
||||||
cfg.Heartbeat.Enabled,
|
cfg.Heartbeat.Enabled,
|
||||||
)
|
)
|
||||||
|
|
@ -635,7 +648,7 @@ func gatewayCmd() {
|
||||||
channel, chatID = "cli", "direct"
|
channel, chatID = "cli", "direct"
|
||||||
}
|
}
|
||||||
// Use ProcessHeartbeat - no session history, each heartbeat is independent
|
// Use ProcessHeartbeat - no session history, each heartbeat is independent
|
||||||
response, err := agentLoop.ProcessHeartbeat(context.Background(), prompt, channel, chatID)
|
response, err := agentLoop.ProcessHeartbeat(appCtx, prompt, channel, chatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tools.ErrorResult(fmt.Sprintf("Heartbeat error: %v", err))
|
return tools.ErrorResult(fmt.Sprintf("Heartbeat error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
@ -693,9 +706,6 @@ func gatewayCmd() {
|
||||||
fmt.Printf("✓ Gateway started on %s:%d\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
fmt.Printf("✓ Gateway started on %s:%d\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||||
fmt.Println("Press Ctrl+C to stop")
|
fmt.Println("Press Ctrl+C to stop")
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := cronService.Start(); err != nil {
|
if err := cronService.Start(); err != nil {
|
||||||
fmt.Printf("Error starting cron service: %v\n", err)
|
fmt.Printf("Error starting cron service: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
@ -706,19 +716,19 @@ func gatewayCmd() {
|
||||||
}
|
}
|
||||||
fmt.Println("✓ Heartbeat service started")
|
fmt.Println("✓ Heartbeat service started")
|
||||||
|
|
||||||
stateManager := state.NewManager(cfg.WorkspacePath())
|
stateManager := state.NewManager(cfg.SandboxPath())
|
||||||
deviceService := devices.NewService(devices.Config{
|
deviceService := devices.NewService(devices.Config{
|
||||||
Enabled: cfg.Devices.Enabled,
|
Enabled: cfg.Devices.Enabled,
|
||||||
MonitorUSB: cfg.Devices.MonitorUSB,
|
MonitorUSB: cfg.Devices.MonitorUSB,
|
||||||
}, stateManager)
|
}, stateManager)
|
||||||
deviceService.SetBus(msgBus)
|
deviceService.SetBus(msgBus)
|
||||||
if err := deviceService.Start(ctx); err != nil {
|
if err := deviceService.Start(appCtx); err != nil {
|
||||||
fmt.Printf("Error starting device service: %v\n", err)
|
fmt.Printf("Error starting device service: %v\n", err)
|
||||||
} else if cfg.Devices.Enabled {
|
} else if cfg.Devices.Enabled {
|
||||||
fmt.Println("✓ Device event service started")
|
fmt.Println("✓ Device event service started")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := channelManager.StartAll(ctx); err != nil {
|
if err := channelManager.StartAll(appCtx); err != nil {
|
||||||
fmt.Printf("Error starting channels: %v\n", err)
|
fmt.Printf("Error starting channels: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -730,7 +740,7 @@ func gatewayCmd() {
|
||||||
}()
|
}()
|
||||||
fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||||
|
|
||||||
go agentLoop.Run(ctx)
|
go agentLoop.Run(appCtx)
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, os.Interrupt)
|
signal.Notify(sigChan, os.Interrupt)
|
||||||
|
|
@ -738,12 +748,15 @@ func gatewayCmd() {
|
||||||
|
|
||||||
fmt.Println("\nShutting down...")
|
fmt.Println("\nShutting down...")
|
||||||
cancel()
|
cancel()
|
||||||
healthServer.Stop(context.Background())
|
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer shutdownCancel()
|
||||||
|
healthServer.Stop(shutdownCtx)
|
||||||
deviceService.Stop()
|
deviceService.Stop()
|
||||||
heartbeatService.Stop()
|
heartbeatService.Stop()
|
||||||
cronService.Stop()
|
cronService.Stop()
|
||||||
agentLoop.Stop()
|
agentLoop.Stop()
|
||||||
channelManager.StopAll(ctx)
|
channelManager.StopAll(shutdownCtx)
|
||||||
fmt.Println("✓ Gateway stopped")
|
fmt.Println("✓ Gateway stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -784,16 +797,7 @@ func memoryMigrateSessions() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.Memory.Enabled {
|
memDBPath := cfg.DBPath()
|
||||||
fmt.Println("Memory system is disabled in config. Enable it first.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace := cfg.WorkspacePath()
|
|
||||||
memDBPath := cfg.Memory.DBPath
|
|
||||||
if memDBPath == "" {
|
|
||||||
memDBPath = filepath.Join(workspace, "memory", "picoclaw.db")
|
|
||||||
}
|
|
||||||
os.MkdirAll(filepath.Dir(memDBPath), 0755)
|
os.MkdirAll(filepath.Dir(memDBPath), 0755)
|
||||||
|
|
||||||
del, err := delegate.NewFromConfig(cfg.Memory, memDBPath)
|
del, err := delegate.NewFromConfig(cfg.Memory, memDBPath)
|
||||||
|
|
@ -809,7 +813,7 @@ func memoryMigrateSessions() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionsDir := filepath.Join(workspace, "sessions")
|
sessionsDir := filepath.Join(cfg.SandboxPath(), "sessions")
|
||||||
stats, err := picomemory.MigrateFileSessions(ctx, del, "picoclaw", sessionsDir)
|
stats, err := picomemory.MigrateFileSessions(ctx, del, "picoclaw", sessionsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Migration error: %v\n", err)
|
fmt.Printf("Migration error: %v\n", err)
|
||||||
|
|
@ -830,11 +834,7 @@ func memoryDBStatus() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace := cfg.WorkspacePath()
|
memDBPath := cfg.DBPath()
|
||||||
memDBPath := cfg.Memory.DBPath
|
|
||||||
if memDBPath == "" {
|
|
||||||
memDBPath = filepath.Join(workspace, "memory", "picoclaw.db")
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err := os.Stat(memDBPath)
|
fi, err := os.Stat(memDBPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -876,11 +876,11 @@ func statusCmd() {
|
||||||
fmt.Println("Config:", configPath, "✗")
|
fmt.Println("Config:", configPath, "✗")
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace := cfg.WorkspacePath()
|
sandboxPath := cfg.SandboxPath()
|
||||||
if _, err := os.Stat(workspace); err == nil {
|
if _, err := os.Stat(sandboxPath); err == nil {
|
||||||
fmt.Println("Workspace:", workspace, "✓")
|
fmt.Println("Sandbox:", sandboxPath, "✓")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Workspace:", workspace, "✗")
|
fmt.Println("Sandbox:", sandboxPath, "✗")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
|
|
@ -926,10 +926,8 @@ func statusCmd() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory system status
|
// Memory system status (always enabled)
|
||||||
fmt.Println("\nMemory System:")
|
fmt.Println("\nMemory System:")
|
||||||
if cfg.Memory.Enabled {
|
|
||||||
fmt.Println(" Enabled: ✓")
|
|
||||||
fmt.Printf(" Embedding dims: %d\n", cfg.Memory.EmbeddingDims)
|
fmt.Printf(" Embedding dims: %d\n", cfg.Memory.EmbeddingDims)
|
||||||
if cfg.Memory.Embedding.Provider != "" {
|
if cfg.Memory.Embedding.Provider != "" {
|
||||||
fmt.Printf(" Embedding provider: %s\n", cfg.Memory.Embedding.Provider)
|
fmt.Printf(" Embedding provider: %s\n", cfg.Memory.Embedding.Provider)
|
||||||
|
|
@ -947,14 +945,11 @@ func statusCmd() {
|
||||||
}
|
}
|
||||||
memDBPath := cfg.Memory.DBPath
|
memDBPath := cfg.Memory.DBPath
|
||||||
if memDBPath == "" {
|
if memDBPath == "" {
|
||||||
memDBPath = filepath.Join(workspace, "memory", "picoclaw.db")
|
memDBPath = filepath.Join(cfg.WorkspacePath(), "memory", "picoclaw.db")
|
||||||
}
|
}
|
||||||
if fi, err := os.Stat(memDBPath); err == nil {
|
if fi, err := os.Stat(memDBPath); err == nil {
|
||||||
fmt.Printf(" DB size: %.1f KB\n", float64(fi.Size())/1024)
|
fmt.Printf(" DB size: %.1f KB\n", float64(fi.Size())/1024)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
fmt.Println(" Enabled: ✗")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1419,13 +1414,13 @@ func daemonStart() {
|
||||||
}
|
}
|
||||||
|
|
||||||
registry := tools.NewToolRegistry()
|
registry := tools.NewToolRegistry()
|
||||||
workspace := cfg.WorkspacePath()
|
sandbox := cfg.SandboxPath()
|
||||||
restrict := cfg.Agents.Defaults.RestrictToWorkspace
|
restrict := cfg.RestrictToSandbox()
|
||||||
registry.Register(tools.NewExecTool(workspace, restrict))
|
registry.Register(tools.NewExecTool(sandbox, restrict))
|
||||||
registry.Register(tools.NewReadFileTool(workspace, restrict))
|
registry.Register(tools.NewReadFileTool(sandbox, restrict))
|
||||||
registry.Register(tools.NewWriteFileTool(workspace, restrict))
|
registry.Register(tools.NewWriteFileTool(sandbox, restrict))
|
||||||
registry.Register(tools.NewListDirTool(workspace, restrict))
|
registry.Register(tools.NewListDirTool(sandbox, restrict))
|
||||||
registry.Register(tools.NewEditFileTool(workspace, restrict))
|
registry.Register(tools.NewEditFileTool(sandbox, restrict))
|
||||||
|
|
||||||
capLookup := func(name string) (tools.ToolCapabilities, bool) {
|
capLookup := func(name string) (tools.ToolCapabilities, bool) {
|
||||||
t, ok := registry.Get(name)
|
t, ok := registry.Get(name)
|
||||||
|
|
@ -1443,7 +1438,7 @@ func daemonStart() {
|
||||||
defer secureBus.Close()
|
defer secureBus.Close()
|
||||||
|
|
||||||
fmt.Printf("picoclaw daemon started (pid=%d, socket=%s)\n", pid, sockPath)
|
fmt.Printf("picoclaw daemon started (pid=%d, socket=%s)\n", pid, sockPath)
|
||||||
fmt.Printf(" workspace: %s\n", workspace)
|
fmt.Printf(" sandbox: %s\n", sandbox)
|
||||||
fmt.Printf(" tools: %d registered\n", len(registry.List()))
|
fmt.Printf(" tools: %d registered\n", len(registry.List()))
|
||||||
fmt.Println("Press Ctrl+C to stop.")
|
fmt.Println("Press Ctrl+C to stop.")
|
||||||
|
|
||||||
|
|
@ -1541,18 +1536,18 @@ func getConfigPath() string {
|
||||||
return filepath.Join(home, ".picoclaw", "config.json")
|
return filepath.Join(home, ".picoclaw", "config.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, cronOpts ...cron.CronOption) *cron.CronService {
|
func setupCronTool(appCtx context.Context, agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, cronOpts ...cron.CronOption) *cron.CronService {
|
||||||
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
||||||
|
|
||||||
cronService := cron.NewCronService(cronStorePath, nil, cronOpts...)
|
cronService := cron.NewCronService(cronStorePath, nil, cronOpts...)
|
||||||
|
|
||||||
// Create and register CronTool
|
|
||||||
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout)
|
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout)
|
||||||
agentLoop.RegisterTool(cronTool)
|
agentLoop.RegisterTool(cronTool)
|
||||||
|
|
||||||
// Set the onJob handler
|
|
||||||
cronService.SetOnJob(func(job *cron.CronJob) (string, error) {
|
cronService.SetOnJob(func(job *cron.CronJob) (string, error) {
|
||||||
result := cronTool.ExecuteJob(context.Background(), job)
|
jobCtx, jobCancel := context.WithTimeout(appCtx, execTimeout)
|
||||||
|
defer jobCancel()
|
||||||
|
result := cronTool.ExecuteJob(jobCtx, job)
|
||||||
return result, nil
|
return result, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1620,7 +1615,7 @@ func cronCmd() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cronStorePath := filepath.Join(cfg.WorkspacePath(), "cron", "jobs.json")
|
cronStorePath := filepath.Join(cfg.SandboxPath(), "cron", "jobs.json")
|
||||||
|
|
||||||
switch subcommand {
|
switch subcommand {
|
||||||
case "list":
|
case "list":
|
||||||
|
|
@ -1924,12 +1919,12 @@ func skillsInstallBuiltinCmd(workspace string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func skillsListBuiltinCmd() {
|
func skillsListBuiltinCmd() {
|
||||||
cfg, err := loadConfig()
|
_, err := loadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error loading config: %v\n", err)
|
fmt.Printf("Error loading config: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
builtinSkillsDir := filepath.Join(filepath.Dir(cfg.WorkspacePath()), "picoclaw", "skills")
|
builtinSkillsDir, _ := config.SkillsDir()
|
||||||
|
|
||||||
fmt.Println("\nAvailable Builtin Skills:")
|
fmt.Println("\nAvailable Builtin Skills:")
|
||||||
fmt.Println("-----------------------")
|
fmt.Println("-----------------------")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue