package mcp import ( "bufio" "fmt" "os" "path/filepath" "strings" ) const tokenFileName = ".tm_token" // TokenFile holds credentials from a workspace .tm_token file. type TokenFile struct { Project string Token string Path string } // FindTokenFile walks from start (usually cwd) upward looking for .tm_token. func FindTokenFile(start string) (TokenFile, error) { dir, err := filepath.Abs(start) if err != nil { return TokenFile{}, err } for { path := filepath.Join(dir, tokenFileName) if st, err := os.Stat(path); err == nil && !st.IsDir() { tf, err := ParseTokenFile(path) if err != nil { return TokenFile{}, err } return tf, nil } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } return TokenFile{}, fmt.Errorf("no %s found (create an MCP token in the TUI with g, then place project=… and token=… in %s)", tokenFileName, tokenFileName) } // ParseTokenFile reads project= and token= lines from path. func ParseTokenFile(path string) (TokenFile, error) { f, err := os.Open(path) if err != nil { return TokenFile{}, err } defer f.Close() tf := TokenFile{Path: path} sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } key, val, ok := strings.Cut(line, "=") if !ok { continue } key = strings.TrimSpace(strings.ToLower(key)) val = strings.TrimSpace(val) switch key { case "project": tf.Project = val case "token": tf.Token = val } } if err := sc.Err(); err != nil { return TokenFile{}, err } if tf.Project == "" || tf.Token == "" { return TokenFile{}, fmt.Errorf("%s must contain project= and token= lines", path) } return tf, nil }