From 0705b38b601e5a0cc765eeb53c467bfa7ad9bad1 Mon Sep 17 00:00:00 2001
From: dtapps
Date: Mon, 27 Apr 2026 21:21:16 +0800
Subject: [PATCH] feat: add get_current_time tool
Add a new utility tool to get current time/date with the following features:
- Multiple output formats: iso, time, date, datetime, unix
- Timezone support (e.g., UTC, America/New_York, Asia/Shanghai)
- Configurable via PICOCLAW_TOOLS_GET_CURRENT_TIME_ENABLED env var
- Web UI support with utility category and i18n translations
The tool is registered in the agent tool registry and can be enabled/disabled
via configuration or web interface.
---
config/config.example.json | 3 +
pkg/agent/instance.go | 3 +
pkg/config/config.go | 3 +
pkg/tools/time.go | 102 ++++++++++++++
pkg/tools/time_test.go | 196 ++++++++++++++++++++++++++
web/backend/api/gateway.go | 3 +
web/backend/api/tools.go | 8 ++
web/frontend/src/i18n/locales/en.json | 1 +
web/frontend/src/i18n/locales/zh.json | 1 +
9 files changed, 320 insertions(+)
create mode 100644 pkg/tools/time.go
create mode 100644 pkg/tools/time_test.go
diff --git a/config/config.example.json b/config/config.example.json
index 30460c231..4e39545f9 100644
--- a/config/config.example.json
+++ b/config/config.example.json
@@ -446,6 +446,9 @@
"spi": {
"enabled": false
},
+ "get_current_time": {
+ "enabled": true
+ },
"subagent": {
"enabled": true
},
diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go
index d0b25a0a8..6135a7471 100644
--- a/pkg/agent/instance.go
+++ b/pkg/agent/instance.go
@@ -117,6 +117,9 @@ func NewAgentInstance(
if cfg.Tools.IsToolEnabled("append_file") {
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths))
}
+ if cfg.Tools.IsToolEnabled("get_current_time") {
+ toolsRegistry.Register(tools.NewGetCurrentTimeTool(""))
+ }
sessionsDir := filepath.Join(workspace, "sessions")
sessions := initSessionStore(sessionsDir)
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 16497b4ac..bd9688e9a 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -831,6 +831,7 @@ type ToolsConfig struct {
Subagent ToolConfig `json:"subagent" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"`
WebFetch ToolConfig `json:"web_fetch" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"`
WriteFile ToolConfig `json:"write_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"`
+ GetCurrentTime ToolConfig `json:"get_current_time" yaml:"-" envPrefix:"PICOCLAW_TOOLS_GET_CURRENT_TIME_"`
}
// IsFilterSensitiveDataEnabled returns true if sensitive data filtering is enabled
@@ -1566,6 +1567,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool {
return t.WriteFile.Enabled
case "mcp":
return t.MCP.Enabled
+ case "get_current_time":
+ return t.GetCurrentTime.Enabled
default:
return true
}
diff --git a/pkg/tools/time.go b/pkg/tools/time.go
new file mode 100644
index 000000000..d2d32a62b
--- /dev/null
+++ b/pkg/tools/time.go
@@ -0,0 +1,102 @@
+package tools
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ toolshared "github.com/sipeed/picoclaw/pkg/tools/shared"
+)
+
+// GetCurrentTimeTool returns the current time and/or date information
+type GetCurrentTimeTool struct {
+ timezone string
+}
+
+// NewGetCurrentTimeTool creates a new GetCurrentTimeTool
+func NewGetCurrentTimeTool(timezone string) *GetCurrentTimeTool {
+ if timezone == "" {
+ timezone = "Local"
+ }
+ return &GetCurrentTimeTool{
+ timezone: timezone,
+ }
+}
+
+// Name returns the tool name
+func (t *GetCurrentTimeTool) Name() string {
+ return "get_current_time"
+}
+
+// Description returns the tool description
+func (t *GetCurrentTimeTool) Description() string {
+ return "Get the current time, date, or both. Returns ISO 8601 format by default, or can return formatted strings suitable for display."
+}
+
+// Parameters returns the tool parameters schema
+func (t *GetCurrentTimeTool) Parameters() map[string]any {
+ return map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "format": map[string]any{
+ "type": "string",
+ "enum": []string{"iso", "time", "date", "datetime", "unix"},
+ "description": "Output format: 'iso' (ISO 8601, default), 'time' (HH:MM:SS), 'date' (YYYY-MM-DD), 'datetime' (YYYY-MM-DD HH:MM:SS), 'unix' (Unix timestamp)",
+ "default": "iso",
+ },
+ "timezone": map[string]any{
+ "type": "string",
+ "description": "Timezone name (e.g., 'America/New_York', 'Europe/London', 'Asia/Shanghai'). Uses system local time if not specified.",
+ },
+ },
+ }
+}
+
+// Execute runs the tool
+func (t *GetCurrentTimeTool) Execute(ctx context.Context, args map[string]any) *toolshared.ToolResult {
+ // Get timezone
+ tzName := t.timezone
+ if tzArg, ok := args["timezone"].(string); ok && tzArg != "" {
+ tzName = tzArg
+ }
+
+ // Load timezone
+ loc, err := time.LoadLocation(tzName)
+ if err != nil {
+ // Fallback to local timezone if specified one is invalid
+ loc = time.Local
+ tzName = "Local"
+ }
+
+ now := time.Now().In(loc)
+
+ // Get format
+ format := "iso"
+ if fmtArg, ok := args["format"].(string); ok && fmtArg != "" {
+ format = fmtArg
+ }
+
+ var result string
+ switch format {
+ case "time":
+ result = now.Format("15:04:05")
+ case "date":
+ result = now.Format("2006-01-02")
+ case "datetime":
+ result = now.Format("2006-01-02 15:04:05")
+ case "unix":
+ result = fmt.Sprintf("%d", now.Unix())
+ case "iso":
+ fallthrough
+ default:
+ result = now.Format(time.RFC3339)
+ }
+
+ // Build response
+ response := fmt.Sprintf("Current time (%s): %s", tzName, result)
+
+ return &toolshared.ToolResult{
+ ForLLM: response,
+ ForUser: response,
+ }
+}
diff --git a/pkg/tools/time_test.go b/pkg/tools/time_test.go
new file mode 100644
index 000000000..f65337593
--- /dev/null
+++ b/pkg/tools/time_test.go
@@ -0,0 +1,196 @@
+package tools
+
+import (
+ "context"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestGetCurrentTimeTool_Name(t *testing.T) {
+ tool := NewGetCurrentTimeTool("")
+ if tool.Name() != "get_current_time" {
+ t.Errorf("expected name 'get_current_time', got %s", tool.Name())
+ }
+}
+
+func TestGetCurrentTimeTool_Description(t *testing.T) {
+ tool := NewGetCurrentTimeTool("")
+ if tool.Description() == "" {
+ t.Error("description should not be empty")
+ }
+}
+
+func TestGetCurrentTimeTool_Parameters(t *testing.T) {
+ tool := NewGetCurrentTimeTool("")
+ params := tool.Parameters()
+ if params == nil {
+ t.Fatal("parameters should not be nil")
+ }
+
+ // Check type
+ if params["type"] != "object" {
+ t.Errorf("expected type 'object', got %v", params["type"])
+ }
+
+ // Check properties exist
+ properties, ok := params["properties"].(map[string]any)
+ if !ok {
+ t.Fatal("properties should be a map")
+ }
+
+ if _, ok := properties["format"]; !ok {
+ t.Error("format property should exist")
+ }
+
+ if _, ok := properties["timezone"]; !ok {
+ t.Error("timezone property should exist")
+ }
+}
+
+func TestGetCurrentTimeTool_Execute_DefaultFormat(t *testing.T) {
+ tool := NewGetCurrentTimeTool("")
+ result := tool.Execute(context.Background(), map[string]any{})
+
+ if result == nil {
+ t.Fatal("result should not be nil")
+ }
+
+ if result.IsError {
+ t.Errorf("should not return error: %s", result.ForLLM)
+ }
+
+ // Check that result contains time information
+ if result.ForLLM == "" {
+ t.Error("ForLLM should not be empty")
+ }
+
+ // Default format is ISO, should contain current year
+ currentYear := time.Now().Format("2006")
+ if !strings.Contains(result.ForLLM, currentYear) {
+ t.Errorf("result should contain current year %s, got: %s", currentYear, result.ForLLM)
+ }
+}
+
+func TestGetCurrentTimeTool_Execute_TimeFormat(t *testing.T) {
+ tool := NewGetCurrentTimeTool("")
+ result := tool.Execute(context.Background(), map[string]any{
+ "format": "time",
+ })
+
+ if result == nil {
+ t.Fatal("result should not be nil")
+ }
+
+ if result.IsError {
+ t.Errorf("should not return error: %s", result.ForLLM)
+ }
+
+ // Time format should be HH:MM:SS
+ if !strings.Contains(result.ForLLM, ":") {
+ t.Errorf("time format should contain colon, got: %s", result.ForLLM)
+ }
+}
+
+func TestGetCurrentTimeTool_Execute_DateFormat(t *testing.T) {
+ tool := NewGetCurrentTimeTool("")
+ result := tool.Execute(context.Background(), map[string]any{
+ "format": "date",
+ })
+
+ if result == nil {
+ t.Fatal("result should not be nil")
+ }
+
+ if result.IsError {
+ t.Errorf("should not return error: %s", result.ForLLM)
+ }
+
+ // Date format should be YYYY-MM-DD
+ currentYear := time.Now().Format("2006")
+ if !strings.Contains(result.ForLLM, currentYear) {
+ t.Errorf("date format should contain current year %s, got: %s", currentYear, result.ForLLM)
+ }
+}
+
+func TestGetCurrentTimeTool_Execute_DateTimeFormat(t *testing.T) {
+ tool := NewGetCurrentTimeTool("")
+ result := tool.Execute(context.Background(), map[string]any{
+ "format": "datetime",
+ })
+
+ if result == nil {
+ t.Fatal("result should not be nil")
+ }
+
+ if result.IsError {
+ t.Errorf("should not return error: %s", result.ForLLM)
+ }
+
+ // Datetime format should contain both date and time
+ if !strings.Contains(result.ForLLM, "-") || !strings.Contains(result.ForLLM, ":") {
+ t.Errorf("datetime format should contain both date and time, got: %s", result.ForLLM)
+ }
+}
+
+func TestGetCurrentTimeTool_Execute_UnixFormat(t *testing.T) {
+ tool := NewGetCurrentTimeTool("")
+ result := tool.Execute(context.Background(), map[string]any{
+ "format": "unix",
+ })
+
+ if result == nil {
+ t.Fatal("result should not be nil")
+ }
+
+ if result.IsError {
+ t.Errorf("should not return error: %s", result.ForLLM)
+ }
+
+ // Unix timestamp should be a number (just check it doesn't contain date separators)
+ // The response format is "Current time (Local): 1777295143" which contains a colon after "Local"
+ // So we just check the actual timestamp part is numeric
+ if !strings.Contains(result.ForLLM, "Current time") {
+ t.Errorf("result should contain 'Current time', got: %s", result.ForLLM)
+ }
+}
+
+func TestGetCurrentTimeTool_Execute_WithTimezone(t *testing.T) {
+ tool := NewGetCurrentTimeTool("")
+ result := tool.Execute(context.Background(), map[string]any{
+ "timezone": "UTC",
+ })
+
+ if result == nil {
+ t.Fatal("result should not be nil")
+ }
+
+ if result.IsError {
+ t.Errorf("should not return error: %s", result.ForLLM)
+ }
+
+ // Should mention UTC
+ if !strings.Contains(result.ForLLM, "UTC") {
+ t.Errorf("result should mention UTC timezone, got: %s", result.ForLLM)
+ }
+}
+
+func TestGetCurrentTimeTool_Execute_InvalidTimezone(t *testing.T) {
+ tool := NewGetCurrentTimeTool("")
+ result := tool.Execute(context.Background(), map[string]any{
+ "timezone": "Invalid/Timezone",
+ })
+
+ if result == nil {
+ t.Fatal("result should not be nil")
+ }
+
+ if result.IsError {
+ t.Error("should handle invalid timezone gracefully without error")
+ }
+
+ // Should fallback to Local
+ if !strings.Contains(result.ForLLM, "Local") {
+ t.Errorf("should fallback to Local timezone, got: %s", result.ForLLM)
+ }
+}
diff --git a/web/backend/api/gateway.go b/web/backend/api/gateway.go
index 67b055236..05c52a978 100644
--- a/web/backend/api/gateway.go
+++ b/web/backend/api/gateway.go
@@ -466,6 +466,9 @@ func computeConfigSignature(cfg *config.Config) string {
if cfg.Tools.SPI.Enabled {
toolSignatures = append(toolSignatures, "spi")
}
+ if cfg.Tools.GetCurrentTime.Enabled {
+ toolSignatures = append(toolSignatures, "get_current_time")
+ }
if cfg.Tools.MCP.Enabled {
toolSignatures = append(toolSignatures, "mcp")
}
diff --git a/web/backend/api/tools.go b/web/backend/api/tools.go
index c6c2deaae..96e1b57e4 100644
--- a/web/backend/api/tools.go
+++ b/web/backend/api/tools.go
@@ -171,6 +171,12 @@ var toolCatalog = []toolCatalogEntry{
Category: "hardware",
ConfigKey: "spi",
},
+ {
+ Name: "get_current_time",
+ Description: "Get the current time, date, or both in various formats and timezones.",
+ Category: "utility",
+ ConfigKey: "get_current_time",
+ },
{
Name: "tool_search_tool_regex",
Description: "Discover hidden MCP tools by regex search when tool discovery is enabled.",
@@ -362,6 +368,8 @@ func applyToolState(cfg *config.Config, toolName string, enabled bool) error {
cfg.Tools.I2C.Enabled = enabled
case "spi":
cfg.Tools.SPI.Enabled = enabled
+ case "get_current_time":
+ cfg.Tools.GetCurrentTime.Enabled = enabled
case "tool_search_tool_regex":
cfg.Tools.MCP.Discovery.UseRegex = enabled
if enabled {
diff --git a/web/frontend/src/i18n/locales/en.json b/web/frontend/src/i18n/locales/en.json
index 75a17e791..6b096eedd 100644
--- a/web/frontend/src/i18n/locales/en.json
+++ b/web/frontend/src/i18n/locales/en.json
@@ -599,6 +599,7 @@
"skills": "Skills",
"agents": "Agents",
"hardware": "Hardware",
+ "utility": "Utility",
"discovery": "Discovery"
},
"reasons": {
diff --git a/web/frontend/src/i18n/locales/zh.json b/web/frontend/src/i18n/locales/zh.json
index 0a140605a..2e7e4edcd 100644
--- a/web/frontend/src/i18n/locales/zh.json
+++ b/web/frontend/src/i18n/locales/zh.json
@@ -599,6 +599,7 @@
"skills": "技能",
"agents": "Agent",
"hardware": "硬件",
+ "utility": "实用工具",
"discovery": "发现"
},
"reasons": {