Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions cmd/tacit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Usage:
tacit status Check daemon status
tacit update Update tacit to the latest version
tacit list [duration] List knowledge entries (default: 24h)
tacit search <pattern> Search knowledge entries by pattern
tacit search [--duration <d>] <pattern> Search knowledge entries by pattern
tacit get <file-path>... Print the full content of one or more knowledge entries
tacit config view Show current configuration
tacit config edit Open configuration in a text editor
Expand Down Expand Up @@ -832,25 +832,54 @@ func cmdList() {

// cmdSearch searches the knowledge base for entries matching a pattern.
func cmdSearch() {
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "Usage: tacit search <pattern>\n")
// Parse args: tacit search [--duration <dur>] <pattern>
args := os.Args[2:]
var since time.Time
var patternArgs []string

for i := 0; i < len(args); i++ {
if args[i] == "--duration" && i+1 < len(args) {
d, err := parseDuration(args[i+1])
if err != nil {
fmt.Fprintf(os.Stderr, "Invalid duration %q: %v\n", args[i+1], err)
fmt.Fprintf(os.Stderr, "Examples: 1h, 30m, 24h, 1d, 7d, 2w\n")
os.Exit(1)
}
since = time.Now().Add(-d)
i++ // skip duration value
} else {
patternArgs = append(patternArgs, args[i])
}
}

if len(patternArgs) == 0 {
fmt.Fprintf(os.Stderr, "Usage: tacit search [--duration <duration>] <pattern>\n")
fmt.Fprintf(os.Stderr, "Examples: tacit search meeting, tacit search --duration 1h meeting\n")
os.Exit(1)
}

pattern := os.Args[2]
pattern := patternArgs[0]
baseDir := config.BaseDir()

results, err := search.Search(baseDir, pattern)
results, err := search.Search(baseDir, pattern, since)
if err != nil {
log.Fatalf("Search failed: %v", err)
}

if len(results) == 0 {
fmt.Printf("No results found for %q.\n", pattern)
if !since.IsZero() {
fmt.Printf("No results found for %q in the last %s.\n", pattern, formatDuration(time.Since(since)))
} else {
fmt.Printf("No results found for %q.\n", pattern)
}
return
}

fmt.Printf("Found %d result(s) for %q:\n\n", len(results), pattern)
if !since.IsZero() {
fmt.Printf("Found %d result(s) for %q in the last %s:\n\n", len(results), pattern, formatDuration(time.Since(since)))
} else {
fmt.Printf("Found %d result(s) for %q:\n\n", len(results), pattern)
}
for _, r := range results {
fmt.Printf("[%s] %s / %s\n", r.CreatedAt.Format("2006-01-02 15:04:05"), r.Category, r.Title)
fmt.Printf(" File: %s\n", r.FilePath)
Expand Down
8 changes: 7 additions & 1 deletion pkg/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"sort"
"strings"
"sync"
"time"

"github.com/sangmin7648/tacit/pkg/storage"
)
Expand Down Expand Up @@ -82,8 +83,9 @@ func isFrontmatterLine(s string) bool {
}

// Search searches the knowledge base at baseDir for pattern (case-insensitive).
// If since is non-zero, only entries created after that time are included.
// Returns results sorted by relevance score descending.
func Search(baseDir, pattern string) ([]*SearchResult, error) {
func Search(baseDir, pattern string, since time.Time) ([]*SearchResult, error) {
rg, err := extractRg()
if err != nil {
return nil, err
Expand Down Expand Up @@ -152,6 +154,10 @@ func Search(baseDir, pattern string) ([]*SearchResult, error) {
continue
}

if !since.IsZero() && !entry.CreatedAt.After(since) {
continue
}

score := len(re.FindAllString(entry.Title, -1)) * 10
score += len(re.FindAllString(entry.Category, -1)) * 5
score += len(re.FindAllString(entry.Summary, -1)) * 3
Expand Down
9 changes: 5 additions & 4 deletions skills/tacit.knowledge/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ You are retrieving relevant knowledge from multiple sources: the local tacit kno
## Available tacit Commands

```
tacit list [duration] — List entries created within duration (default: 1h). Supports: 30m, 1h, 24h, 1d, 7d, 2w
tacit search <pattern> — Full-text search across all entries (title + summary + content)
tacit get <file-path> — Print full content of a specific entry
tacit list [duration] — List entries created within duration (default: 1h). Supports: 30m, 1h, 24h, 1d, 7d, 2w
tacit search [--duration <d>] <pattern> — Full-text search across all entries (title + summary + content). Optional --duration limits to entries created within that window.
tacit get <file-path> — Print full content of a specific entry
```

## Process
Expand All @@ -37,7 +37,7 @@ Launch one sub-agent per source, all in parallel using the Agent tool. Each sub-

**tacit sub-agent** — retrieves from local knowledge base:
1. Run `tacit list <duration>` to get recent entries
2. Extract 2–4 keywords from the user's prompt and run `tacit search <keyword>` for each
2. Extract 2–4 keywords from the user's prompt and run `tacit search <keyword>` for each. If a time window was identified, pass `--duration <d>` to narrow results (e.g. `tacit search --duration 1h <keyword>`)
3. Merge all unique file paths, prioritizing entries that appear in both list and search results
4. Fetch full content: `tacit get <path1> <path2> ...`
5. Return structured results: `{ source: "tacit", items: [{ title, file_path, date, category, summary, content }] }`
Expand Down Expand Up @@ -70,4 +70,5 @@ Synthesize all retrieved knowledge into a direct answer to the user's prompt:
- tacit categories and content are primarily in Korean
- Do NOT fabricate knowledge entries — only reference what actually exists
- `tacit search` supports regex-compatible patterns — you can use `tacit search "키워드1\|키워드2"` to search multiple terms at once
- `tacit search --duration` accepts the same units as `tacit list`: `30m`, `1h`, `24h`, `1d`, `7d`, `2w`
- When launching external source sub-agents, pass the user's original prompt and extracted keywords so each sub-agent can independently determine the best query strategy for its source