Skip to content
85 changes: 73 additions & 12 deletions cmd/engram/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,7 @@ func cmdTUI(cfg store.Config) {

func cmdSearch(cfg store.Config) {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: engram search <query> [--type TYPE] [--project PROJECT|--all] [--scope SCOPE] [--limit N] [--match all|any]")
fmt.Fprintln(os.Stderr, "usage: engram search <query> [--type TYPE] [--project PROJECT|--all] [--scope SCOPE] [--org ORG] [--limit N] [--match all|any]")
exitFunc(1)
}

Expand Down Expand Up @@ -1260,6 +1260,14 @@ func cmdSearch(cfg store.Config) {
opts.MatchMode = os.Args[i+1]
i++
}
case "--org":
if i+1 >= len(os.Args) || strings.HasPrefix(os.Args[i+1], "-") {
fmt.Fprintln(os.Stderr, "error: --org requires a value")
exitFunc(1)
return
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
opts.Org = strings.TrimSpace(os.Args[i+1])
i++
default:
queryParts = append(queryParts, os.Args[i])
}
Expand Down Expand Up @@ -1301,16 +1309,20 @@ func cmdSearch(cfg store.Config) {
if r.Project != nil {
project = fmt.Sprintf(" | project: %s", *r.Project)
}
fmt.Printf("[%d] #%d (%s) β€” %s\n %s\n %s%s | scope: %s\n\n",
org := ""
if r.Org != nil {
org = fmt.Sprintf(" | org: %s", *r.Org)
}
fmt.Printf("[%d] #%d (%s) β€” %s\n %s\n %s%s | scope: %s%s\n\n",
i+1, r.ID, r.Type, r.Title,
truncate(r.Content, 300),
timeutil.FormatLocal(r.CreatedAt), project, r.Scope)
timeutil.FormatLocal(r.CreatedAt), project, r.Scope, org)
}
}

func cmdSave(cfg store.Config) {
if len(os.Args) < 4 {
fmt.Fprintln(os.Stderr, "usage: engram save <title> <content> [--type TYPE] [--project PROJECT] [--scope SCOPE] [--topic TOPIC_KEY]")
fmt.Fprintln(os.Stderr, "usage: engram save <title> <content> [--type TYPE] [--project PROJECT] [--scope SCOPE] [--topic TOPIC_KEY] [--org ORG]")
exitFunc(1)
}

Expand All @@ -1320,6 +1332,7 @@ func cmdSave(cfg store.Config) {
projectName := ""
scope := "project"
topicKey := ""
org := ""

for i := 4; i < len(os.Args); i++ {
switch os.Args[i] {
Expand All @@ -1343,6 +1356,14 @@ func cmdSave(cfg store.Config) {
topicKey = os.Args[i+1]
i++
}
case "--org":
if i+1 >= len(os.Args) || strings.HasPrefix(os.Args[i+1], "-") {
fmt.Fprintln(os.Stderr, "error: --org requires a value")
exitFunc(1)
return
}
org = strings.TrimSpace(os.Args[i+1])
i++
}
}

Expand Down Expand Up @@ -1393,6 +1414,15 @@ func cmdSave(cfg store.Config) {
return
}

// --org wins when given explicitly; otherwise inherit from the nearest
// .engram/config.json org field for this cwd, independent of how the
// project name itself was resolved (#776).
if strings.TrimSpace(org) == "" {
if cfgResult := detectProjectFull(cwd); cfgResult.Org != "" {
org = cfgResult.Org
}
}

s, err := storeNew(cfg)
if err != nil {
fatal(err)
Expand All @@ -1411,6 +1441,7 @@ func cmdSave(cfg store.Config) {
Project: projectName,
Scope: scope,
TopicKey: topicKey,
Org: org,
})
if err != nil {
fatal(err)
Expand Down Expand Up @@ -2265,6 +2296,7 @@ func cmdObsidianExport(cfg store.Config) {
var (
vault string
project string
org string
limit int
since string
force bool
Expand All @@ -2286,6 +2318,14 @@ func cmdObsidianExport(cfg store.Config) {
project = os.Args[i+1]
i++
}
case "--org":
if i+1 >= len(os.Args) || strings.HasPrefix(os.Args[i+1], "-") {
fmt.Fprintln(os.Stderr, "error: --org requires a value")
exitFunc(1)
return
}
org = strings.TrimSpace(os.Args[i+1])
i++
case "--all":
allProjects = true
case "--limit":
Expand Down Expand Up @@ -2364,6 +2404,7 @@ func cmdObsidianExport(cfg store.Config) {
exportCfg := obsidian.ExportConfig{
VaultPath: vault,
Project: project,
Org: org,
Limit: limit,
Force: force,
GraphConfig: graphMode,
Expand Down Expand Up @@ -2562,13 +2603,27 @@ func cmdProjectsRescueOwnership(cfg store.Config) {
}

func cmdProjectsList(cfg store.Config) {
org := ""
for i := 3; i < len(os.Args); i++ {
switch os.Args[i] {
case "--org":
if i+1 >= len(os.Args) || strings.HasPrefix(os.Args[i+1], "-") {
fmt.Fprintln(os.Stderr, "error: --org requires a value")
exitFunc(1)
return
}
org = strings.TrimSpace(os.Args[i+1])
i++
}
}

s, err := storeNew(cfg)
if err != nil {
fatal(err)
}
defer s.Close()

projects, err := s.ListProjectsWithStats()
projects, err := s.ListProjectsWithStats(org)
if err != nil {
fatal(err)
}
Expand All @@ -2578,7 +2633,11 @@ func cmdProjectsList(cfg store.Config) {
return
}

fmt.Printf("Projects (%d):\n", len(projects))
if org != "" {
fmt.Printf("Projects (%d) β€” org: %s\n", len(projects), org)
} else {
fmt.Printf("Projects (%d):\n", len(projects))
}
for _, p := range projects {
sessionWord := "sessions"
if p.SessionCount == 1 {
Expand Down Expand Up @@ -2749,7 +2808,7 @@ func cmdProjectsConsolidate(cfg store.Config) {
// Only normalization-equivalent legacy names are safe automatic candidates.
similar := findNormalizationEquivalentProjects(canonical, allNames)

allStats, _ := s.ListProjectsWithStats()
allStats, _ := s.ListProjectsWithStats("")
statsMap := make(map[string]store.ProjectStats)
for _, ps := range allStats {
statsMap[ps.Name] = ps
Expand Down Expand Up @@ -2838,7 +2897,7 @@ func cmdProjectsConsolidate(cfg store.Config) {
}

// --all mode: group all projects by normalization equivalence.
projects, err := s.ListProjectsWithStats()
projects, err := s.ListProjectsWithStats("")
if err != nil {
fatal(err)
}
Expand Down Expand Up @@ -2993,7 +3052,7 @@ func cmdProjectsPrune(cfg store.Config) {
}
defer s.Close()

allStats, err := s.ListProjectsWithStats()
allStats, err := s.ListProjectsWithStats("")
if err != nil {
fatal(err)
}
Expand Down Expand Up @@ -3541,8 +3600,8 @@ Commands:
test [suite] [--quick] [--json]
Run isolated local reliability and performance self-tests
suites: reliability, performance (default: both)
search <query> Search memories [--type TYPE] [--project PROJECT|--all] [--scope SCOPE] [--limit N] [--match all|any]
save <title> <msg> Save a memory [--type TYPE] [--project PROJECT] [--scope SCOPE]
search <query> Search memories [--type TYPE] [--project PROJECT|--all] [--scope SCOPE] [--org ORG] [--limit N] [--match all|any]
save <title> <msg> Save a memory [--type TYPE] [--project PROJECT] [--scope SCOPE] [--org ORG]
delete <obs_id> Delete an observation [--hard] (soft-delete by default; --hard removes permanently)
delete session <id>
Delete a session by ID (session must have no observations)
Expand Down Expand Up @@ -3570,7 +3629,8 @@ Commands:
import <file> Import memories from a JSON export file
init [name] Initialize an Engram project (.engram/config.json) in current directory
--force, -f Overwrite existing .engram/config.json
projects list List all projects with observation, session, and prompt counts
projects list [--org ORG]
List all projects with observation, session, and prompt counts
projects consolidate [--all] [--dry-run]
Merge similar project names into one canonical name
--all Scan ALL projects for similar name groups
Expand All @@ -3597,6 +3657,7 @@ Commands:
Export memories to an Obsidian-compatible markdown vault
--vault Path to Obsidian vault root (required)
--project Filter export to a single project (optional; cannot combine with --all)
--org Filter export to a single org (optional)
--all Export every project
--limit Cap exported observations at N (optional)
--since Export only observations after this date, e.g. 2026-01-01 (optional)
Expand Down
Loading